You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1681 lines
60 KiB
1681 lines
60 KiB
/** |
|
* @file options.cpp |
|
* |
|
* Load and save options from the diablo.ini file. |
|
*/ |
|
#include "options.h" |
|
|
|
#include <algorithm> |
|
#include <cerrno> |
|
#include <cstdint> |
|
#include <cstdio> |
|
#include <cstring> |
|
#include <functional> |
|
#include <iterator> |
|
#include <optional> |
|
#include <span> |
|
#include <string> |
|
#include <unordered_set> |
|
|
|
#ifdef USE_SDL3 |
|
#include <SDL3/SDL_audio.h> |
|
#include <SDL3/SDL_keycode.h> |
|
#include <SDL3/SDL_stdinc.h> |
|
#include <SDL3/SDL_version.h> |
|
#else |
|
#include <SDL_version.h> |
|
#endif |
|
|
|
#include <expected.hpp> |
|
#include <fmt/format.h> |
|
#include <function_ref.hpp> |
|
|
|
#include "appfat.h" |
|
#include "controls/control_mode.hpp" |
|
#include "controls/controller_buttons.h" |
|
#include "engine/assets.hpp" |
|
#include "engine/sound_defs.hpp" |
|
#include "platform/locale.hpp" |
|
#include "quick_messages.hpp" |
|
#include "utils/algorithm/container.hpp" |
|
#include "utils/file_util.h" |
|
#include "utils/ini.hpp" |
|
#include "utils/language.h" |
|
#include "utils/log.hpp" |
|
#include "utils/logged_fstream.hpp" |
|
#include "utils/paths.h" |
|
#include "utils/sdl_ptrs.h" |
|
#include "utils/str_cat.hpp" |
|
#include "utils/str_split.hpp" |
|
#include "utils/utf8.hpp" |
|
|
|
namespace devilution { |
|
|
|
#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 3 |
|
#endif |
|
#ifndef DEFAULT_PER_PIXEL_LIGHTING |
|
#define DEFAULT_PER_PIXEL_LIGHTING true |
|
#endif |
|
|
|
namespace { |
|
|
|
void DiscoverMods() |
|
{ |
|
// Add mods available by default: |
|
std::unordered_set<std::string> modNames = { "clock", "adria_refills_mana", "Floating Numbers - Damage", "Floating Numbers - XP" }; |
|
|
|
if (HaveHellfire()) { |
|
modNames.insert("Hellfire"); |
|
} |
|
|
|
// Check if the mods directory exists. |
|
const std::string modsPath = StrCat(paths::PrefPath(), "mods"); |
|
if (DirectoryExists(modsPath.c_str())) { |
|
// Find unpacked mods |
|
for (const std::string &modFolder : ListDirectories(modsPath.c_str())) { |
|
// Only consider this folder if the init.lua file exists. |
|
const std::string modScriptPath = modsPath + DIRECTORY_SEPARATOR_STR + modFolder + DIRECTORY_SEPARATOR_STR + "lua" + DIRECTORY_SEPARATOR_STR + "mods" + DIRECTORY_SEPARATOR_STR + modFolder + DIRECTORY_SEPARATOR_STR + "init.lua"; |
|
if (!FileExists(modScriptPath.c_str())) |
|
continue; |
|
|
|
modNames.insert(modFolder); |
|
} |
|
|
|
// Find packed mods |
|
for (const std::string &modMpq : ListFiles(modsPath.c_str())) { |
|
if (!modMpq.ends_with(".mpq")) |
|
continue; |
|
|
|
modNames.insert(modMpq.substr(0, modMpq.size() - 4)); |
|
} |
|
} |
|
|
|
// Get the list of mods currently stored in the INI. |
|
std::vector<std::string_view> existingMods = GetOptions().Mods.GetModList(); |
|
|
|
// Add new mods. |
|
for (const std::string &modName : modNames) { |
|
if (std::find(existingMods.begin(), existingMods.end(), modName) == existingMods.end()) |
|
GetOptions().Mods.AddModEntry(modName); |
|
} |
|
|
|
// Remove mods that are no longer installed. |
|
for (const std::string_view &modName : existingMods) { |
|
if (modNames.find(std::string(modName)) == modNames.end()) |
|
GetOptions().Mods.RemoveModEntry(std::string(modName)); |
|
} |
|
} |
|
|
|
std::optional<Ini> ini; |
|
|
|
#if defined(__ANDROID__) || (defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE == 1) |
|
constexpr OptionEntryFlags OnlyIfSupportsWindowed = OptionEntryFlags::Invisible; |
|
#else |
|
constexpr OptionEntryFlags OnlyIfSupportsWindowed = OptionEntryFlags::None; |
|
#endif |
|
|
|
constexpr size_t NumResamplers = |
|
#ifdef DEVILUTIONX_RESAMPLER_SPEEX |
|
1 + |
|
#endif |
|
#ifdef DVL_AULIB_SUPPORTS_SDL_RESAMPLER |
|
1 + |
|
#endif |
|
0; |
|
|
|
std::string GetIniPath() |
|
{ |
|
auto path = paths::ConfigPath() + std::string("diablo.ini"); |
|
return path; |
|
} |
|
|
|
void LoadIni() |
|
{ |
|
std::vector<char> buffer; |
|
auto path = GetIniPath(); |
|
FILE *file = OpenFile(path.c_str(), "rb"); |
|
if (file != nullptr) { |
|
uintmax_t size; |
|
if (GetFileSize(path.c_str(), &size)) { |
|
buffer.resize(static_cast<size_t>(size)); |
|
if (std::fread(buffer.data(), static_cast<size_t>(size), 1, file) != 1) { |
|
const char *errorMessage = std::strerror(errno); |
|
if (errorMessage == nullptr) errorMessage = ""; |
|
LogError(LogCategory::System, "std::fread: failed with \"{}\"", errorMessage); |
|
buffer.clear(); |
|
} |
|
} |
|
std::fclose(file); |
|
} |
|
tl::expected<Ini, std::string> result = Ini::parse(std::string_view(buffer.data(), buffer.size())); |
|
if (!result.has_value()) app_fatal(result.error()); |
|
ini.emplace(std::move(result).value()); |
|
} |
|
|
|
void SaveIni() |
|
{ |
|
if (!ini.has_value()) return; |
|
if (!ini->changed()) return; |
|
if (!paths::ConfigPath().empty()) { |
|
RecursivelyCreateDir(paths::ConfigPath().c_str()); |
|
} |
|
const std::string iniPath = GetIniPath(); |
|
LoggedFStream out; |
|
if (!out.Open(iniPath.c_str(), "wb")) { |
|
LogError("Failed to open ini file for writing at {}: {}", iniPath, std::strerror(errno)); |
|
return; |
|
} |
|
const std::string newContents = ini->serialize(); |
|
if (out.Write(newContents.data(), newContents.size())) { |
|
ini->markAsUnchanged(); |
|
} |
|
out.Close(); |
|
} |
|
|
|
#if SDL_VERSION_ATLEAST(2, 0, 0) |
|
bool HardwareCursorDefault() |
|
{ |
|
#if defined(__ANDROID__) || (defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE == 1) |
|
// See https://github.com/diasurgical/devilutionX/issues/2502 |
|
return false; |
|
#else |
|
return HardwareCursorSupported(); |
|
#endif |
|
} |
|
#endif |
|
|
|
} // namespace |
|
|
|
Options &GetOptions() |
|
{ |
|
static Options options; |
|
return options; |
|
} |
|
|
|
#if SDL_VERSION_ATLEAST(2, 0, 0) |
|
bool HardwareCursorSupported() |
|
{ |
|
#if (defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE == 1) || __DJGPP__ |
|
return false; |
|
#elif USE_SDL3 |
|
return true; |
|
#else |
|
SDL_version v; |
|
SDL_GetVersion(&v); |
|
return SDL_VERSIONNUM(v.major, v.minor, v.patch) >= SDL_VERSIONNUM(2, 0, 12); |
|
#endif |
|
} |
|
#endif |
|
|
|
void LoadOptions() |
|
{ |
|
LoadIni(); |
|
DiscoverMods(); |
|
Options &options = GetOptions(); |
|
for (OptionCategoryBase *pCategory : options.GetCategories()) { |
|
for (OptionEntryBase *pEntry : pCategory->GetEntries()) { |
|
pEntry->LoadFromIni(pCategory->GetKey()); |
|
} |
|
} |
|
|
|
ini->getUtf8Buf("Hellfire", "SItem", options.Hellfire.szItem, sizeof(options.Hellfire.szItem)); |
|
ini->getUtf8Buf("Network", "Bind Address", "0.0.0.0", options.Network.szBindAddress, sizeof(options.Network.szBindAddress)); |
|
ini->getUtf8Buf("Network", "Previous Game ID", options.Network.szPreviousZTGame, sizeof(options.Network.szPreviousZTGame)); |
|
ini->getUtf8Buf("Network", "Previous Host", options.Network.szPreviousHost, sizeof(options.Network.szPreviousHost)); |
|
|
|
for (size_t i = 0; i < QuickMessages.size(); i++) { |
|
const std::span<const Ini::Value> values = ini->get("NetMsg", QuickMessages[i].key); |
|
std::vector<std::string> &result = options.Chat.szHotKeyMsgs[i]; |
|
result.clear(); |
|
result.reserve(values.size()); |
|
for (const Ini::Value &value : values) { |
|
result.emplace_back(value.value); |
|
} |
|
} |
|
|
|
ini->getUtf8Buf("Controller", "Mapping", options.Controller.szMapping, sizeof(options.Controller.szMapping)); |
|
options.Controller.fDeadzone = ini->getFloat("Controller", "deadzone", 0.07F); |
|
#ifdef __vita__ |
|
options.Controller.bRearTouch = ini->getBool("Controller", "Enable Rear Touchpad", true); |
|
#endif |
|
} |
|
|
|
void SaveOptions() |
|
{ |
|
Options &options = GetOptions(); |
|
for (OptionCategoryBase *pCategory : options.GetCategories()) { |
|
for (const OptionEntryBase *pEntry : pCategory->GetEntries()) { |
|
pEntry->SaveToIni(pCategory->GetKey()); |
|
} |
|
} |
|
|
|
ini->set("Hellfire", "SItem", options.Hellfire.szItem); |
|
|
|
ini->set("Network", "Bind Address", options.Network.szBindAddress); |
|
ini->set("Network", "Previous Game ID", options.Network.szPreviousZTGame); |
|
ini->set("Network", "Previous Host", options.Network.szPreviousHost); |
|
|
|
for (size_t i = 0; i < QuickMessages.size(); i++) { |
|
ini->set("NetMsg", QuickMessages[i].key, options.Chat.szHotKeyMsgs[i]); |
|
} |
|
|
|
ini->set("Controller", "Mapping", options.Controller.szMapping); |
|
ini->set("Controller", "deadzone", options.Controller.fDeadzone); |
|
#ifdef __vita__ |
|
ini->set("Controller", "Enable Rear Touchpad", options.Controller.bRearTouch); |
|
#endif |
|
|
|
SaveIni(); |
|
} |
|
|
|
std::string_view OptionEntryBase::GetName() const |
|
{ |
|
return _(name); |
|
} |
|
std::string_view OptionEntryBase::GetDescription() const |
|
{ |
|
return _(description); |
|
} |
|
OptionEntryFlags OptionEntryBase::GetFlags() const |
|
{ |
|
return flags; |
|
} |
|
void OptionEntryBase::SetValueChangedCallback(tl::function_ref<void()> callback) |
|
{ |
|
callback_ = callback; |
|
} |
|
void OptionEntryBase::NotifyValueChanged() |
|
{ |
|
if (callback_.has_value()) (*callback_)(); |
|
} |
|
|
|
void OptionEntryBoolean::LoadFromIni(std::string_view category) |
|
{ |
|
value = ini->getBool(category, key, defaultValue); |
|
} |
|
void OptionEntryBoolean::SaveToIni(std::string_view category) const |
|
{ |
|
ini->set(category, key, value); |
|
} |
|
void OptionEntryBoolean::SetValue(bool newValue) |
|
{ |
|
this->value = newValue; |
|
this->NotifyValueChanged(); |
|
} |
|
OptionEntryType OptionEntryBoolean::GetType() const |
|
{ |
|
return OptionEntryType::Boolean; |
|
} |
|
std::string_view OptionEntryBoolean::GetValueDescription() const |
|
{ |
|
return value ? _("ON") : _("OFF"); |
|
} |
|
|
|
OptionEntryType OptionEntryListBase::GetType() const |
|
{ |
|
return OptionEntryType::List; |
|
} |
|
std::string_view OptionEntryListBase::GetValueDescription() const |
|
{ |
|
return GetListDescription(GetActiveListIndex()); |
|
} |
|
|
|
void OptionEntryEnumBase::LoadFromIni(std::string_view category) |
|
{ |
|
value = ini->getInt(category, key, defaultValue); |
|
} |
|
void OptionEntryEnumBase::SaveToIni(std::string_view category) const |
|
{ |
|
ini->set(category, key, value); |
|
} |
|
void OptionEntryEnumBase::SetValueInternal(int newValue) |
|
{ |
|
this->value = newValue; |
|
this->NotifyValueChanged(); |
|
} |
|
void OptionEntryEnumBase::AddEntry(int entryValue, std::string_view name) |
|
{ |
|
entryValues.push_back(entryValue); |
|
entryNames.push_back(name); |
|
} |
|
size_t OptionEntryEnumBase::GetListSize() const |
|
{ |
|
return entryValues.size(); |
|
} |
|
std::string_view OptionEntryEnumBase::GetListDescription(size_t index) const |
|
{ |
|
return _(entryNames[index].data()); |
|
} |
|
size_t OptionEntryEnumBase::GetActiveListIndex() const |
|
{ |
|
auto iterator = c_find(entryValues, 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(std::string_view category) |
|
{ |
|
value = ini->getInt(category, key, defaultValue); |
|
if (c_find(entryValues, value) == entryValues.end()) { |
|
entryValues.insert(c_lower_bound(entryValues, value), value); |
|
entryNames.clear(); |
|
} |
|
} |
|
void OptionEntryIntBase::SaveToIni(std::string_view category) const |
|
{ |
|
ini->set(category, key, value); |
|
} |
|
void OptionEntryIntBase::SetValueInternal(int newValue) |
|
{ |
|
this->value = newValue; |
|
this->NotifyValueChanged(); |
|
} |
|
void OptionEntryIntBase::AddEntry(int entryValue) |
|
{ |
|
entryValues.push_back(entryValue); |
|
} |
|
size_t OptionEntryIntBase::GetListSize() const |
|
{ |
|
return entryValues.size(); |
|
} |
|
std::string_view OptionEntryIntBase::GetListDescription(size_t index) const |
|
{ |
|
if (entryNames.empty()) { |
|
for (auto entryValue : entryValues) { |
|
entryNames.push_back(StrCat(entryValue)); |
|
} |
|
} |
|
return entryNames[index].data(); |
|
} |
|
size_t OptionEntryIntBase::GetActiveListIndex() const |
|
{ |
|
auto iterator = c_find(entryValues, 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(); |
|
} |
|
|
|
std::string_view OptionCategoryBase::GetKey() const |
|
{ |
|
return key; |
|
} |
|
std::string_view OptionCategoryBase::GetName() const |
|
{ |
|
return _(name); |
|
} |
|
std::string_view OptionCategoryBase::GetDescription() const |
|
{ |
|
return _(description); |
|
} |
|
|
|
GameModeOptions::GameModeOptions() |
|
: OptionCategoryBase("GameMode", N_("Game Mode"), N_("Game Mode Settings")) |
|
, gameMode("Game", OptionEntryFlags::Invisible, N_("Game Mode"), N_("Play Diablo or Hellfire."), StartUpGameMode::Ask, |
|
{ |
|
{ StartUpGameMode::Diablo, N_("Diablo") }, |
|
// Ask is missing, because 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) |
|
|
|
{ |
|
} |
|
std::vector<OptionEntryBase *> GameModeOptions::GetEntries() |
|
{ |
|
return { |
|
&gameMode, |
|
&shareware, |
|
}; |
|
} |
|
|
|
StartUpOptions::StartUpOptions() |
|
: OptionCategoryBase("StartUp", N_("Start Up"), N_("Start Up Settings")) |
|
, diabloIntro("Diablo Intro", OptionEntryFlags::OnlyDiablo, N_("Intro"), N_("Shown Intro cinematic."), StartUpIntro::Once, |
|
{ |
|
{ StartUpIntro::Off, N_("OFF") }, |
|
// Once is missing, because 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, because 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") }, |
|
}) |
|
{ |
|
} |
|
std::vector<OptionEntryBase *> StartUpOptions::GetEntries() |
|
{ |
|
return { |
|
&diabloIntro, |
|
&hellfireIntro, |
|
&splash, |
|
}; |
|
} |
|
|
|
DiabloOptions::DiabloOptions() |
|
: OptionCategoryBase("Diablo", N_("Diablo"), N_("Diablo specific Settings")) |
|
, lastSinglePlayerHero("LastSinglePlayerHero", OptionEntryFlags::Invisible | OptionEntryFlags::OnlyDiablo, "Sample Rate", "Remembers what singleplayer hero/save was last used.", 0) |
|
, lastMultiplayerHero("LastMultiplayerHero", OptionEntryFlags::Invisible | OptionEntryFlags::OnlyDiablo, "Sample Rate", "Remembers what multiplayer hero/save was last used.", 0) |
|
{ |
|
} |
|
std::vector<OptionEntryBase *> DiabloOptions::GetEntries() |
|
{ |
|
return { |
|
&lastSinglePlayerHero, |
|
&lastMultiplayerHero, |
|
}; |
|
} |
|
|
|
HellfireOptions::HellfireOptions() |
|
: OptionCategoryBase("Hellfire", N_("Hellfire"), N_("Hellfire specific Settings")) |
|
, lastSinglePlayerHero("LastSinglePlayerHero", OptionEntryFlags::Invisible | OptionEntryFlags::OnlyHellfire, "Sample Rate", "Remembers what singleplayer hero/save was last used.", 0) |
|
, lastMultiplayerHero("LastMultiplayerHero", OptionEntryFlags::Invisible | OptionEntryFlags::OnlyHellfire, "Sample Rate", "Remembers what multiplayer hero/save was last used.", 0) |
|
{ |
|
} |
|
std::vector<OptionEntryBase *> HellfireOptions::GetEntries() |
|
{ |
|
return { |
|
&lastSinglePlayerHero, |
|
&lastMultiplayerHero, |
|
}; |
|
} |
|
|
|
AudioOptions::AudioOptions() |
|
: OptionCategoryBase("Audio", N_("Audio"), N_("Audio Settings")) |
|
, soundVolume("Sound Volume", OptionEntryFlags::Invisible, "Sound Volume", "Movie and SFX volume.", VOLUME_MAX) |
|
, audioCuesVolume("Audio Cues Volume", OptionEntryFlags::Invisible, "Audio Cues Volume", "Navigation audio cues volume.", VOLUME_MAX) |
|
, musicVolume("Music Volume", OptionEntryFlags::Invisible, "Music Volume", "Music Volume.", VOLUME_MAX) |
|
, 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 5 (highest)."), DEFAULT_AUDIO_RESAMPLING_QUALITY, { 0, 1, 2, 3, 4, 5 }) |
|
{ |
|
} |
|
std::vector<OptionEntryBase *> AudioOptions::GetEntries() |
|
{ |
|
// clang-format off |
|
return { |
|
&soundVolume, |
|
&audioCuesVolume, |
|
&musicVolume, |
|
&walkingSound, |
|
&autoEquipSound, |
|
&itemPickupSound, |
|
&sampleRate, |
|
&channels, |
|
&bufferSize, |
|
&resampler, |
|
&resamplingQuality, |
|
#if SDL_VERSION_ATLEAST(2, 0, 0) |
|
&device, |
|
#endif |
|
}; |
|
// clang-format on |
|
} |
|
|
|
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(std::string_view category) |
|
{ |
|
size_ = { ini->getInt(category, "Width", DEFAULT_WIDTH), ini->getInt(category, "Height", DEFAULT_HEIGHT) }; |
|
} |
|
void OptionEntryResolution::SaveToIni(std::string_view category) const |
|
{ |
|
ini->set(category, "Width", size_.width); |
|
ini->set(category, "Height", size_.height); |
|
} |
|
|
|
size_t OptionEntryResolution::GetListSize() const |
|
{ |
|
return resolutions_.size(); |
|
} |
|
std::string_view OptionEntryResolution::GetListDescription(size_t index) const |
|
{ |
|
return resolutions_[index].second; |
|
} |
|
size_t OptionEntryResolution::GetActiveListIndex() const |
|
{ |
|
auto found = c_find_if(resolutions_, [this](const auto &x) { return x.first == size_; }); |
|
if (found == resolutions_.end()) |
|
return 0; |
|
return std::distance(resolutions_.begin(), found); |
|
} |
|
void OptionEntryResolution::SetActiveListIndex(size_t index) |
|
{ |
|
size_ = resolutions_[index].first; |
|
NotifyValueChanged(); |
|
} |
|
|
|
OptionEntryResampler::OptionEntryResampler() |
|
: OptionEntryListBase("Resampler", OptionEntryFlags::CantChangeInGame |
|
// When there are exactly 2 options there is no submenu, so we need to recreate the UI |
|
// to reflect the change in the "Resampling quality" setting visibility. |
|
| (NumResamplers == 2 ? OptionEntryFlags::RecreateUI : OptionEntryFlags::None), |
|
N_("Resampler"), N_("Audio resampler")) |
|
{ |
|
} |
|
void OptionEntryResampler::LoadFromIni(std::string_view category) |
|
{ |
|
const std::string_view resamplerStr = ini->getString(category, key); |
|
if (!resamplerStr.empty()) { |
|
std::optional<Resampler> resampler = ResamplerFromString(resamplerStr); |
|
if (resampler) { |
|
resampler_ = *resampler; |
|
UpdateDependentOptions(); |
|
return; |
|
} |
|
} |
|
resampler_ = Resampler::DEVILUTIONX_DEFAULT_RESAMPLER; |
|
UpdateDependentOptions(); |
|
} |
|
|
|
void OptionEntryResampler::SaveToIni(std::string_view category) const |
|
{ |
|
ini->set(category, key, ResamplerToString(resampler_)); |
|
} |
|
|
|
size_t OptionEntryResampler::GetListSize() const |
|
{ |
|
return NumResamplers; |
|
} |
|
|
|
std::string_view OptionEntryResampler::GetListDescription(size_t index) const |
|
{ |
|
return ResamplerToString(static_cast<Resampler>(index)); |
|
} |
|
|
|
size_t OptionEntryResampler::GetActiveListIndex() const |
|
{ |
|
return static_cast<size_t>(resampler_); |
|
} |
|
|
|
void OptionEntryResampler::SetActiveListIndex(size_t index) |
|
{ |
|
resampler_ = static_cast<Resampler>(index); |
|
UpdateDependentOptions(); |
|
NotifyValueChanged(); |
|
} |
|
|
|
void OptionEntryResampler::UpdateDependentOptions() const |
|
{ |
|
#ifdef DEVILUTIONX_RESAMPLER_SPEEX |
|
if (resampler_ == Resampler::Speex) { |
|
GetOptions().Audio.resamplingQuality.flags &= ~OptionEntryFlags::Invisible; |
|
} else { |
|
GetOptions().Audio.resamplingQuality.flags |= OptionEntryFlags::Invisible; |
|
} |
|
#endif |
|
} |
|
|
|
OptionEntryAudioDevice::OptionEntryAudioDevice() |
|
: OptionEntryListBase("Device", OptionEntryFlags::CantChangeInGame, N_("Device"), N_("Audio device")) |
|
{ |
|
} |
|
void OptionEntryAudioDevice::LoadFromIni(std::string_view category) |
|
{ |
|
deviceName_ = ini->getString(category, key); |
|
} |
|
|
|
void OptionEntryAudioDevice::SaveToIni(std::string_view category) const |
|
{ |
|
#if SDL_VERSION_ATLEAST(2, 0, 0) |
|
ini->set(category, key, deviceName_); |
|
#endif |
|
} |
|
|
|
size_t OptionEntryAudioDevice::GetListSize() const |
|
{ |
|
#if defined(USE_SDL3) |
|
int numDevices = 0; |
|
SDLUniquePtr<SDL_AudioDeviceID> devices { SDL_GetAudioPlaybackDevices(&numDevices) }; |
|
return static_cast<size_t>(numDevices) + 1; |
|
#elif SDL_VERSION_ATLEAST(2, 0, 0) |
|
return SDL_GetNumAudioDevices(false) + 1; |
|
#else |
|
return 1; |
|
#endif |
|
} |
|
|
|
std::string_view OptionEntryAudioDevice::GetListDescription(size_t index) const |
|
{ |
|
std::string_view deviceName = GetDeviceName(index); |
|
if (deviceName.empty()) deviceName = "System Default"; |
|
return deviceName; |
|
} |
|
|
|
size_t OptionEntryAudioDevice::GetActiveListIndex() const |
|
{ |
|
#ifdef USE_SDL3 |
|
int numDevices; |
|
SDLUniquePtr<SDL_AudioDeviceID> devices { SDL_GetAudioPlaybackDevices(&numDevices) }; |
|
if (devices == nullptr) return 0; |
|
for (int i = 0; i < numDevices; ++i) { |
|
const char *deviceName = SDL_GetAudioDeviceName(devices.get()[i]); |
|
if (deviceName_ == deviceName) return i; |
|
} |
|
return 0; |
|
#else |
|
for (size_t i = 0; i < GetListSize(); i++) { |
|
const std::string_view deviceName = GetDeviceName(i); |
|
if (deviceName_ == deviceName) return i; |
|
} |
|
return 0; |
|
#endif |
|
} |
|
|
|
void OptionEntryAudioDevice::SetActiveListIndex(size_t index) |
|
{ |
|
deviceName_ = std::string { GetDeviceName(index) }; |
|
NotifyValueChanged(); |
|
} |
|
|
|
std::string_view OptionEntryAudioDevice::GetDeviceName(size_t index) const |
|
{ |
|
if (index == 0) return {}; // System Default |
|
#if defined(USE_SDL3) |
|
int numDevices = 0; |
|
SDLUniquePtr<SDL_AudioDeviceID> devices { SDL_GetAudioPlaybackDevices(&numDevices) }; |
|
if (devices == nullptr || static_cast<int>(index) > numDevices) return "Unknown"; |
|
const char *deviceName = SDL_GetAudioDeviceName(devices.get()[index - 1]); |
|
if (deviceName == nullptr) return "Unknown"; |
|
return deviceName; |
|
#elif SDL_VERSION_ATLEAST(2, 0, 0) |
|
return SDL_GetAudioDeviceName(static_cast<int>(index) - 1, false); |
|
#endif |
|
return {}; |
|
} |
|
|
|
#ifdef USE_SDL3 |
|
SDL_AudioDeviceID OptionEntryAudioDevice::id() const |
|
{ |
|
if (deviceName_.empty()) return SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK; |
|
int numDevices = 0; |
|
SDLUniquePtr<SDL_AudioDeviceID> devices { SDL_GetAudioPlaybackDevices(&numDevices) }; |
|
if (devices == nullptr) { |
|
LogWarn("Failed to get audio devices: {}", SDL_GetError()); |
|
SDL_ClearError(); |
|
return SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK; |
|
} |
|
for (int i = 0; i < numDevices; ++i) { |
|
const SDL_AudioDeviceID id = devices.get()[i]; |
|
if (deviceName_ == SDL_GetAudioDeviceName(id)) return id; |
|
} |
|
return SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK; |
|
} |
|
#endif |
|
|
|
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."), |
|
#ifdef __DJGPP__ |
|
false |
|
#else |
|
true |
|
#endif |
|
) |
|
#endif |
|
#ifndef USE_SDL1 |
|
, upscale("Upscale", OptionEntryFlags::Invisible | 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."), |
|
#if defined(NXDK) || defined(__DJGPP__) |
|
false |
|
#else |
|
true |
|
#endif |
|
) |
|
, 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) |
|
#endif |
|
, frameRateControl("Frame Rate Control", |
|
OptionEntryFlags::RecreateUI |
|
#if defined(NXDK) || defined(__ANDROID__) |
|
| OptionEntryFlags::Invisible |
|
#endif |
|
, |
|
N_("Frame Rate Control"), |
|
N_("Manages frame rate to balance performance, reduce tearing, or save power."), |
|
#if defined(NXDK) || defined(USE_SDL1) |
|
FrameRateControl::CPUSleep |
|
#else |
|
FrameRateControl::VerticalSync |
|
#endif |
|
, |
|
{ |
|
{ FrameRateControl::None, N_("None") }, |
|
#ifndef USE_SDL1 |
|
{ FrameRateControl::VerticalSync, N_("Vertical Sync") }, |
|
#endif |
|
{ FrameRateControl::CPUSleep, N_("Limit FPS") }, |
|
}) |
|
, brightness("Brightness Correction", OptionEntryFlags::Invisible, "Brightness Correction", "Brightness correction level.", 0) |
|
, zoom("Zoom", OptionEntryFlags::None, N_("Zoom"), N_("Zoom on when enabled."), false) |
|
, perPixelLighting("Per-pixel Lighting", OptionEntryFlags::None, N_("Per-pixel Lighting"), N_("Subtile lighting for smoother light gradients."), DEFAULT_PER_PIXEL_LIGHTING) |
|
#ifdef __DREAMCAST__ |
|
, colorCycling("Color Cycling", OptionEntryFlags::None, N_("Color Cycling"), N_("Color cycling effect used for water, lava, and acid animation."), false) |
|
#else |
|
, colorCycling("Color Cycling", OptionEntryFlags::None, N_("Color Cycling"), N_("Color cycling effect used for water, lava, and acid animation."), true) |
|
#endif |
|
, alternateNestArt("Alternate nest art", OptionEntryFlags::OnlyHellfire | OptionEntryFlags::CantChangeInGame, N_("Alternate nest art"), N_("The game will use an alternative palette for Hellfire’s nest tileset."), false) |
|
#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 |
|
#ifdef __DREAMCAST__ |
|
, showFPS("Show FPS", OptionEntryFlags::None, N_("Show FPS"), N_("Displays the FPS in the upper left corner of the screen."), true) |
|
#else |
|
, showFPS("Show FPS", OptionEntryFlags::None, N_("Show FPS"), N_("Displays the FPS in the upper left corner of the screen."), false) |
|
#endif |
|
{ |
|
} |
|
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, |
|
#endif |
|
&frameRateControl, |
|
&brightness, |
|
&zoom, |
|
&showFPS, |
|
&perPixelLighting, |
|
&colorCycling, |
|
&alternateNestArt, |
|
#if SDL_VERSION_ATLEAST(2, 0, 0) |
|
&hardwareCursor, |
|
&hardwareCursorForItems, |
|
&hardwareCursorMaxSize, |
|
#endif |
|
}; |
|
// clang-format on |
|
} |
|
|
|
GameplayOptions::GameplayOptions() |
|
: OptionCategoryBase("Game", N_("Gameplay"), N_("Gameplay Settings")) |
|
, tickRate("Speed", OptionEntryFlags::Invisible, "Speed", "Gameplay ticks per second.", 20) |
|
, 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) |
|
, pauseOnFocusLoss("Pause Game When Window Loses Focus", OptionEntryFlags::None, N_("Pause Game When Window Loses Focus"), N_("When enabled, the game will pause when focus is lost."), true) |
|
, 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) |
|
, multiplayerFullQuests("MultiplayerFullQuests", OptionEntryFlags::CantChangeInMultiPlayer, N_("Full quests in Multiplayer"), N_("Enables the full/uncut singleplayer version of quests."), false) |
|
, testBard("Test Bard", OptionEntryFlags::CantChangeInGame | OptionEntryFlags::OnlyHellfire, N_("Test Bard"), N_("Force the Bard character type to appear in the hero selection menu."), false) |
|
, testBarbarian("Test Barbarian", OptionEntryFlags::CantChangeInGame | OptionEntryFlags::OnlyHellfire, 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) |
|
, showItemGraphicsInStores("Show Item Graphics in Stores", OptionEntryFlags::None, N_("Show Item Graphics in Stores"), N_("Show item graphics to the left of item descriptions in store menus."), false) |
|
, showHealthValues("Show health values", OptionEntryFlags::None, N_("Show health values"), N_("Displays current / max health value on health globe."), false) |
|
, showManaValues("Show mana values", OptionEntryFlags::None, N_("Show mana values"), N_("Displays current / max mana value on mana globe."), false) |
|
, showMultiplayerPartyInfo("Show Multiplayer Party Information", OptionEntryFlags::CantChangeInMultiPlayer, N_("Show Party Information"), N_("Displays the health and mana of all connected multiplayer party members."), false) |
|
, enemyHealthBar("Enemy Health Bar", OptionEntryFlags::None, N_("Enemy Health Bar"), N_("Enemy Health Bar is displayed at the top of the screen."), false) |
|
, floatingInfoBox("Floating Item Info Box", OptionEntryFlags::None, N_("Floating Item Info Box"), N_("Displays item info in a floating box when hovering over an item."), 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) |
|
, autoOilPickup("Auto Oil Pickup", OptionEntryFlags::OnlyHellfire, N_("Auto Oil Pickup"), N_("Oils 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) |
|
, 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) |
|
, showItemLabels("Show Item Labels", OptionEntryFlags::None, N_("Show Item Labels"), N_("Show labels for items on the ground when enabled."), 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, Sacred Shrines and Murphy's 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 }) |
|
, skipLoadingScreenThresholdMs("Skip loading screen threshold, ms", OptionEntryFlags::Invisible, "", "", 0) |
|
{ |
|
} |
|
|
|
std::vector<OptionEntryBase *> GameplayOptions::GetEntries() |
|
{ |
|
return { |
|
&tickRate, |
|
&friendlyFire, |
|
&multiplayerFullQuests, |
|
&randomizeQuests, |
|
&theoQuest, |
|
&cowQuest, |
|
&runInTown, |
|
&quickCast, |
|
&testBard, |
|
&testBarbarian, |
|
&experienceBar, |
|
&showItemGraphicsInStores, |
|
&showHealthValues, |
|
&showManaValues, |
|
&showMultiplayerPartyInfo, |
|
&enemyHealthBar, |
|
&floatingInfoBox, |
|
&showMonsterType, |
|
&showItemLabels, |
|
&autoRefillBelt, |
|
&autoEquipWeapons, |
|
&autoEquipArmor, |
|
&autoEquipHelms, |
|
&autoEquipShields, |
|
&autoEquipJewelry, |
|
&autoGoldPickup, |
|
&autoElixirPickup, |
|
&autoOilPickup, |
|
&numHealPotionPickup, |
|
&numFullHealPotionPickup, |
|
&numManaPotionPickup, |
|
&numFullManaPotionPickup, |
|
&numRejuPotionPickup, |
|
&numFullRejuPotionPickup, |
|
&autoPickupInTown, |
|
&disableCripplingShrines, |
|
&grabInput, |
|
&pauseOnFocusLoss, |
|
&skipLoadingScreenThresholdMs, |
|
}; |
|
} |
|
|
|
ControllerOptions::ControllerOptions() |
|
: OptionCategoryBase("Controller", N_("Controller"), N_("Controller Settings")) |
|
{ |
|
} |
|
std::vector<OptionEntryBase *> ControllerOptions::GetEntries() |
|
{ |
|
return {}; |
|
} |
|
|
|
NetworkOptions::NetworkOptions() |
|
: OptionCategoryBase("Network", N_("Network"), N_("Network Settings")) |
|
, port("Port", OptionEntryFlags::Invisible, "Port", "What network port to use.", 6112) |
|
{ |
|
} |
|
std::vector<OptionEntryBase *> NetworkOptions::GetEntries() |
|
{ |
|
return { |
|
&port, |
|
}; |
|
} |
|
|
|
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(std::string_view category) |
|
{ |
|
ini->getUtf8Buf(category, key, szCode, sizeof(szCode)); |
|
if (szCode[0] != '\0' && 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 = GetLocales(); |
|
|
|
// So that the correct language is shown in the settings menu for users with US english set as a preferred language |
|
// we need to replace the "en_US" locale code with the neutral string "en" as expected by the available options |
|
std::replace(locales.begin(), locales.end(), std::string { "en_US" }, std::string { "en" }); |
|
|
|
// 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) { |
|
const 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: {}", fmt::join(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(std::string_view category) const |
|
{ |
|
ini->set(category, key, szCode); |
|
} |
|
|
|
void OptionEntryLanguageCode::CheckLanguagesAreInitialized() const |
|
{ |
|
if (!languages.empty()) |
|
return; |
|
const bool haveExtraFonts = HaveExtraFonts(); |
|
|
|
// Add well-known supported languages |
|
languages.emplace_back("da", "Dansk"); |
|
languages.emplace_back("de", "Deutsch"); |
|
languages.emplace_back("et", "Eesti"); |
|
languages.emplace_back("en", "English"); |
|
languages.emplace_back("es", "Español"); |
|
languages.emplace_back("fr", "Français"); |
|
languages.emplace_back("hr", "Hrvatski"); |
|
languages.emplace_back("it", "Italiano"); |
|
languages.emplace_back("hu", "Magyar"); |
|
languages.emplace_back("pl", "Polski"); |
|
languages.emplace_back("pt_BR", "Português do Brasil"); |
|
languages.emplace_back("ro", "Română"); |
|
languages.emplace_back("fi", "Suomi"); |
|
languages.emplace_back("sv", "Svenska"); |
|
languages.emplace_back("tr", "Türkçe"); |
|
languages.emplace_back("cs", "Čeština"); |
|
languages.emplace_back("el", "Ελληνικά"); |
|
languages.emplace_back("be", "беларуская"); |
|
languages.emplace_back("bg", "Български"); |
|
languages.emplace_back("ru", "Русский"); |
|
languages.emplace_back("uk", "Українська"); |
|
|
|
if (haveExtraFonts) { |
|
languages.emplace_back("ja", "日本語"); |
|
languages.emplace_back("ko", "한국어"); |
|
languages.emplace_back("zh_CN", "汉语"); |
|
languages.emplace_back("zh_TW", "漢語"); |
|
} |
|
|
|
// 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 (c_find_if(languages, [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(); |
|
} |
|
std::string_view OptionEntryLanguageCode::GetListDescription(size_t index) const |
|
{ |
|
CheckLanguagesAreInitialized(); |
|
return languages[index].second; |
|
} |
|
size_t OptionEntryLanguageCode::GetActiveListIndex() const |
|
{ |
|
CheckLanguagesAreInitialized(); |
|
auto found = c_find_if(languages, [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) |
|
{ |
|
CopyUtf8(szCode, languages[index].first, sizeof(szCode)); |
|
NotifyValueChanged(); |
|
} |
|
|
|
LanguageOptions::LanguageOptions() |
|
: OptionCategoryBase("Language", N_("Language"), N_("Language Settings")) |
|
{ |
|
} |
|
std::vector<OptionEntryBase *> LanguageOptions::GetEntries() |
|
{ |
|
return { |
|
&code, |
|
}; |
|
} |
|
|
|
KeymapperOptions::KeymapperOptions() |
|
: OptionCategoryBase("Keymapping", N_("Keymapping"), N_("Keymapping Settings")) |
|
{ |
|
// Insert all supported keys: a-z, 0-9 and F1-F24. |
|
keyIDToKeyName.reserve(('Z' - 'A' + 1) + ('9' - '0' + 1) + 12); |
|
for (char c = 'A'; c <= 'Z'; ++c) { |
|
keyIDToKeyName.emplace(c, std::string(1, c)); |
|
} |
|
for (char c = '0'; c <= '9'; ++c) { |
|
keyIDToKeyName.emplace(c, std::string(1, c)); |
|
} |
|
for (int i = 0; i < 12; ++i) { |
|
keyIDToKeyName.emplace(SDLK_F1 + i, StrCat("F", i + 1)); |
|
} |
|
for (int i = 0; i < 12; ++i) { |
|
keyIDToKeyName.emplace(SDLK_F13 + i, StrCat("F", i + 13)); |
|
} |
|
|
|
keyIDToKeyName.emplace(SDLK_KP_0, "KEYPADNUM 0"); |
|
for (int i = 0; i < 9; i++) { |
|
keyIDToKeyName.emplace(SDLK_KP_1 + i, StrCat("KEYPADNUM ", i + 1)); |
|
} |
|
|
|
keyIDToKeyName.emplace(SDLK_LALT, "LALT"); |
|
keyIDToKeyName.emplace(SDLK_RALT, "RALT"); |
|
|
|
keyIDToKeyName.emplace(SDLK_SPACE, "SPACE"); |
|
|
|
keyIDToKeyName.emplace(SDLK_RCTRL, "RCONTROL"); |
|
keyIDToKeyName.emplace(SDLK_LCTRL, "LCONTROL"); |
|
|
|
keyIDToKeyName.emplace(SDLK_PRINTSCREEN, "PRINT"); |
|
keyIDToKeyName.emplace(SDLK_PAUSE, "PAUSE"); |
|
keyIDToKeyName.emplace(SDLK_TAB, "TAB"); |
|
keyIDToKeyName.emplace(SDL_BUTTON_MIDDLE | KeymapperMouseButtonMask, "MMOUSE"); |
|
keyIDToKeyName.emplace(SDL_BUTTON_X1 | KeymapperMouseButtonMask, "X1MOUSE"); |
|
keyIDToKeyName.emplace(SDL_BUTTON_X2 | KeymapperMouseButtonMask, "X2MOUSE"); |
|
keyIDToKeyName.emplace(MouseScrollUpButton, "SCROLLUPMOUSE"); |
|
keyIDToKeyName.emplace(MouseScrollDownButton, "SCROLLDOWNMOUSE"); |
|
keyIDToKeyName.emplace(MouseScrollLeftButton, "SCROLLLEFTMOUSE"); |
|
keyIDToKeyName.emplace(MouseScrollRightButton, "SCROLLRIGHTMOUSE"); |
|
|
|
keyIDToKeyName.emplace(SDLK_GRAVE, "`"); |
|
keyIDToKeyName.emplace(SDLK_LEFTBRACKET, "["); |
|
keyIDToKeyName.emplace(SDLK_RIGHTBRACKET, "]"); |
|
keyIDToKeyName.emplace(SDLK_BACKSLASH, "\\"); |
|
keyIDToKeyName.emplace(SDLK_SEMICOLON, ";"); |
|
keyIDToKeyName.emplace(SDLK_APOSTROPHE, "'"); |
|
keyIDToKeyName.emplace(SDLK_COMMA, ","); |
|
keyIDToKeyName.emplace(SDLK_PERIOD, "."); |
|
keyIDToKeyName.emplace(SDLK_SLASH, "/"); |
|
|
|
keyIDToKeyName.emplace(SDLK_BACKSPACE, "BACKSPACE"); |
|
keyIDToKeyName.emplace(SDLK_CAPSLOCK, "CAPSLOCK"); |
|
keyIDToKeyName.emplace(SDLK_SCROLLLOCK, "SCROLLLOCK"); |
|
keyIDToKeyName.emplace(SDLK_INSERT, "INSERT"); |
|
keyIDToKeyName.emplace(SDLK_DELETE, "DELETE"); |
|
keyIDToKeyName.emplace(SDLK_HOME, "HOME"); |
|
keyIDToKeyName.emplace(SDLK_END, "END"); |
|
|
|
keyIDToKeyName.emplace(SDLK_KP_DIVIDE, "KEYPAD /"); |
|
keyIDToKeyName.emplace(SDLK_KP_MULTIPLY, "KEYPAD *"); |
|
keyIDToKeyName.emplace(SDLK_KP_ENTER, "KEYPAD ENTER"); |
|
keyIDToKeyName.emplace(SDLK_KP_PERIOD, "KEYPAD DECIMAL"); |
|
|
|
keyNameToKeyID.reserve(keyIDToKeyName.size()); |
|
for (const auto &[key, value] : keyIDToKeyName) { |
|
keyNameToKeyID.emplace(value, key); |
|
} |
|
} |
|
|
|
std::vector<OptionEntryBase *> KeymapperOptions::GetEntries() |
|
{ |
|
std::vector<OptionEntryBase *> entries; |
|
for (Action &action : actions) { |
|
entries.push_back(&action); |
|
} |
|
return entries; |
|
} |
|
|
|
KeymapperOptions::Action::Action(std::string_view key, const char *name, const char *description, uint32_t defaultKey, std::function<void()> actionPressed, std::function<void()> actionReleased, std::function<bool()> enable, unsigned index) |
|
: OptionEntryBase(key, OptionEntryFlags::None, name, description) |
|
, actionPressed(std::move(actionPressed)) |
|
, actionReleased(std::move(actionReleased)) |
|
, defaultKey(defaultKey) |
|
, enable(std::move(enable)) |
|
, dynamicIndex(index) |
|
{ |
|
if (index != 0) { |
|
dynamicKey = fmt::format(fmt::runtime(std::string_view(key.data(), key.size())), index); |
|
this->key = dynamicKey; |
|
} |
|
} |
|
|
|
std::string_view KeymapperOptions::Action::GetName() const |
|
{ |
|
if (dynamicIndex == 0) |
|
return _(name); |
|
dynamicName = fmt::format(fmt::runtime(_(name)), dynamicIndex); |
|
return dynamicName; |
|
} |
|
|
|
void KeymapperOptions::Action::LoadFromIni(std::string_view category) |
|
{ |
|
const std::span<const Ini::Value> iniValues = ini->get(category, key); |
|
if (iniValues.empty()) { |
|
SetValue(defaultKey); |
|
return; // Use the default key if no key has been set. |
|
} |
|
|
|
const std::string_view iniValue = iniValues.back().value; |
|
if (iniValue.empty()) { |
|
SetValue(SDLK_UNKNOWN); |
|
return; |
|
} |
|
|
|
auto keyIt = GetOptions().Keymapper.keyNameToKeyID.find(iniValue); |
|
if (keyIt == GetOptions().Keymapper.keyNameToKeyID.end()) { |
|
// Use the default key if the key is unknown. |
|
Log("Keymapper: unknown key '{}'", iniValue); |
|
SetValue(defaultKey); |
|
return; |
|
} |
|
|
|
// Store the key in action.key and in the map so we can save() the |
|
// actions while keeping the same order as they have been added. |
|
SetValue(keyIt->second); |
|
} |
|
void KeymapperOptions::Action::SaveToIni(std::string_view category) const |
|
{ |
|
if (boundKey == SDLK_UNKNOWN) { |
|
// Just add an empty config entry if the action is unbound. |
|
ini->set(category, key, std::string {}); |
|
return; |
|
} |
|
auto keyNameIt = GetOptions().Keymapper.keyIDToKeyName.find(boundKey); |
|
if (keyNameIt == GetOptions().Keymapper.keyIDToKeyName.end()) { |
|
LogVerbose("Keymapper: no name found for key {} bound to {}", boundKey, key); |
|
return; |
|
} |
|
ini->set(category, key, keyNameIt->second); |
|
} |
|
|
|
std::string_view KeymapperOptions::Action::GetValueDescription() const |
|
{ |
|
if (boundKey == SDLK_UNKNOWN) |
|
return ""; |
|
auto keyNameIt = GetOptions().Keymapper.keyIDToKeyName.find(boundKey); |
|
if (keyNameIt == GetOptions().Keymapper.keyIDToKeyName.end()) { |
|
return ""; |
|
} |
|
return keyNameIt->second; |
|
} |
|
|
|
bool KeymapperOptions::Action::SetValue(int value) |
|
{ |
|
if (value != SDLK_UNKNOWN && GetOptions().Keymapper.keyIDToKeyName.find(value) == GetOptions().Keymapper.keyIDToKeyName.end()) { |
|
// Ignore invalid key values |
|
return false; |
|
} |
|
|
|
// Remove old key |
|
if (boundKey != SDLK_UNKNOWN) { |
|
GetOptions().Keymapper.keyIDToAction.erase(boundKey); |
|
boundKey = SDLK_UNKNOWN; |
|
} |
|
|
|
// Add new key |
|
if (value != SDLK_UNKNOWN) { |
|
auto it = GetOptions().Keymapper.keyIDToAction.find(value); |
|
if (it != GetOptions().Keymapper.keyIDToAction.end()) { |
|
// Warn about overwriting keys. |
|
Log("Keymapper: key '{}' is already bound to action '{}', overwriting", value, it->second.get().name); |
|
it->second.get().boundKey = SDLK_UNKNOWN; |
|
} |
|
|
|
GetOptions().Keymapper.keyIDToAction.insert_or_assign(value, *this); |
|
boundKey = value; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
void KeymapperOptions::AddAction(std::string_view key, const char *name, const char *description, uint32_t defaultKey, std::function<void()> actionPressed, std::function<void()> actionReleased, std::function<bool()> enable, unsigned index) |
|
{ |
|
actions.emplace_front(key, name, description, defaultKey, std::move(actionPressed), std::move(actionReleased), std::move(enable), index); |
|
} |
|
|
|
void KeymapperOptions::CommitActions() |
|
{ |
|
actions.reverse(); |
|
} |
|
|
|
const KeymapperOptions::Action *KeymapperOptions::findAction(uint32_t key) const |
|
{ |
|
auto it = keyIDToAction.find(key); |
|
if (it == keyIDToAction.end()) return nullptr; |
|
return &it->second.get(); |
|
} |
|
|
|
std::string_view KeymapperOptions::KeyNameForAction(std::string_view actionName) const |
|
{ |
|
for (const Action &action : actions) { |
|
if (action.key == actionName && action.boundKey != SDLK_UNKNOWN) { |
|
return action.GetValueDescription(); |
|
} |
|
} |
|
return ""; |
|
} |
|
|
|
uint32_t KeymapperOptions::KeyForAction(std::string_view actionName) const |
|
{ |
|
for (const Action &action : actions) { |
|
if (action.key == actionName && action.boundKey != SDLK_UNKNOWN) { |
|
return action.boundKey; |
|
} |
|
} |
|
return SDLK_UNKNOWN; |
|
} |
|
|
|
PadmapperOptions::PadmapperOptions() |
|
: OptionCategoryBase("Padmapping", N_("Padmapping"), N_("Padmapping Settings")) |
|
, buttonToButtonName { { |
|
/*ControllerButton_NONE*/ {}, |
|
/*ControllerButton_IGNORE*/ {}, |
|
/*ControllerButton_AXIS_TRIGGERLEFT*/ "LT", |
|
/*ControllerButton_AXIS_TRIGGERRIGHT*/ "RT", |
|
/*ControllerButton_BUTTON_A*/ "A", |
|
/*ControllerButton_BUTTON_B*/ "B", |
|
/*ControllerButton_BUTTON_X*/ "X", |
|
/*ControllerButton_BUTTON_Y*/ "Y", |
|
/*ControllerButton_BUTTON_LEFTSTICK*/ "LS", |
|
/*ControllerButton_BUTTON_RIGHTSTICK*/ "RS", |
|
/*ControllerButton_BUTTON_LEFTSHOULDER*/ "LB", |
|
/*ControllerButton_BUTTON_RIGHTSHOULDER*/ "RB", |
|
/*ControllerButton_BUTTON_START*/ "Start", |
|
/*ControllerButton_BUTTON_BACK*/ "Select", |
|
/*ControllerButton_BUTTON_DPAD_UP*/ "Up", |
|
/*ControllerButton_BUTTON_DPAD_DOWN*/ "Down", |
|
/*ControllerButton_BUTTON_DPAD_LEFT*/ "Left", |
|
/*ControllerButton_BUTTON_DPAD_RIGHT*/ "Right", |
|
} } |
|
{ |
|
buttonNameToButton.reserve(buttonToButtonName.size()); |
|
for (size_t i = 0; i < buttonToButtonName.size(); ++i) { |
|
buttonNameToButton.emplace(buttonToButtonName[i], static_cast<ControllerButton>(i)); |
|
} |
|
} |
|
|
|
std::vector<OptionEntryBase *> PadmapperOptions::GetEntries() |
|
{ |
|
std::vector<OptionEntryBase *> entries; |
|
for (Action &action : actions) { |
|
entries.push_back(&action); |
|
} |
|
return entries; |
|
} |
|
|
|
PadmapperOptions::Action::Action(std::string_view key, const char *name, const char *description, ControllerButtonCombo defaultInput, std::function<void()> actionPressed, std::function<void()> actionReleased, std::function<bool()> enable, unsigned index) |
|
: OptionEntryBase(key, OptionEntryFlags::None, name, description) |
|
, actionPressed(std::move(actionPressed)) |
|
, actionReleased(std::move(actionReleased)) |
|
, defaultInput(defaultInput) |
|
, enable(std::move(enable)) |
|
, dynamicIndex(index) |
|
{ |
|
if (index != 0) { |
|
dynamicKey = fmt::format(fmt::runtime(std::string_view(key.data(), key.size())), index); |
|
this->key = dynamicKey; |
|
} |
|
} |
|
|
|
std::string_view PadmapperOptions::Action::GetName() const |
|
{ |
|
if (dynamicIndex == 0) |
|
return _(name); |
|
dynamicName = fmt::format(fmt::runtime(_(name)), dynamicIndex); |
|
return dynamicName; |
|
} |
|
|
|
void PadmapperOptions::Action::LoadFromIni(std::string_view category) |
|
{ |
|
const std::span<const Ini::Value> iniValues = ini->get(category, key); |
|
if (iniValues.empty()) { |
|
SetValue(defaultInput); |
|
return; // Use the default button combo if no mapping has been set. |
|
} |
|
const std::string_view iniValue = iniValues.back().value; |
|
|
|
std::string modName; |
|
std::string buttonName; |
|
auto parts = SplitByChar(iniValue, '+'); |
|
auto it = parts.begin(); |
|
if (it == parts.end()) { |
|
SetValue(ControllerButtonCombo {}); |
|
return; |
|
} |
|
buttonName = std::string(*it); |
|
if (++it != parts.end()) { |
|
modName = std::move(buttonName); |
|
buttonName = std::string(*it); |
|
} |
|
|
|
ControllerButtonCombo input {}; |
|
if (!modName.empty()) { |
|
auto modifierIt = GetOptions().Padmapper.buttonNameToButton.find(modName); |
|
if (modifierIt == GetOptions().Padmapper.buttonNameToButton.end()) { |
|
// Use the default button combo if the modifier name is unknown. |
|
LogWarn("Padmapper: unknown button '{}'", modName); |
|
SetValue(defaultInput); |
|
return; |
|
} |
|
input.modifier = modifierIt->second; |
|
} |
|
|
|
auto buttonIt = GetOptions().Padmapper.buttonNameToButton.find(buttonName); |
|
if (buttonIt == GetOptions().Padmapper.buttonNameToButton.end()) { |
|
// Use the default button combo if the button name is unknown. |
|
LogWarn("Padmapper: unknown button '{}'", buttonName); |
|
SetValue(defaultInput); |
|
return; |
|
} |
|
input.button = buttonIt->second; |
|
|
|
// Store the input in action.boundInput and in the map so we can save() |
|
// the actions while keeping the same order as they have been added. |
|
SetValue(input); |
|
} |
|
void PadmapperOptions::Action::SaveToIni(std::string_view category) const |
|
{ |
|
if (boundInput.button == ControllerButton_NONE) { |
|
// Just add an empty config entry if the action is unbound. |
|
ini->set(category, key, ""); |
|
return; |
|
} |
|
std::string inputName = GetOptions().Padmapper.buttonToButtonName[static_cast<size_t>(boundInput.button)]; |
|
if (inputName.empty()) { |
|
LogVerbose("Padmapper: no name found for button {} bound to {}", static_cast<size_t>(boundInput.button), key); |
|
return; |
|
} |
|
if (boundInput.modifier != ControllerButton_NONE) { |
|
const std::string &modifierName = GetOptions().Padmapper.buttonToButtonName[static_cast<size_t>(boundInput.modifier)]; |
|
if (modifierName.empty()) { |
|
LogVerbose("Padmapper: no name found for modifier button {} bound to {}", static_cast<size_t>(boundInput.button), key); |
|
return; |
|
} |
|
inputName = StrCat(modifierName, "+", inputName); |
|
} |
|
ini->set(category, key, inputName.data()); |
|
} |
|
|
|
void PadmapperOptions::Action::UpdateValueDescription() const |
|
{ |
|
boundInputDescriptionType = GamepadType; |
|
if (boundInput.button == ControllerButton_NONE) { |
|
boundInputDescription = ""; |
|
boundInputShortDescription = ""; |
|
return; |
|
} |
|
const std::string_view buttonName = ToString(GamepadType, boundInput.button); |
|
if (boundInput.modifier == ControllerButton_NONE) { |
|
boundInputDescription = std::string(buttonName); |
|
boundInputShortDescription = std::string(Shorten(buttonName)); |
|
return; |
|
} |
|
const std::string_view modifierName = ToString(GamepadType, boundInput.modifier); |
|
boundInputDescription = StrCat(modifierName, "+", buttonName); |
|
boundInputShortDescription = StrCat(Shorten(modifierName), "+", Shorten(buttonName)); |
|
} |
|
|
|
std::string_view PadmapperOptions::Action::Shorten(std::string_view buttonName) const |
|
{ |
|
size_t index = 0; |
|
size_t chars = 0; |
|
while (index < buttonName.size()) { |
|
if (!IsTrailUtf8CodeUnit(buttonName[index])) |
|
chars++; |
|
if (chars == 3) |
|
break; |
|
index++; |
|
} |
|
return std::string_view(buttonName.data(), index); |
|
} |
|
|
|
std::string_view PadmapperOptions::Action::GetValueDescription() const |
|
{ |
|
return GetValueDescription(false); |
|
} |
|
|
|
std::string_view PadmapperOptions::Action::GetValueDescription(bool useShortName) const |
|
{ |
|
if (GamepadType != boundInputDescriptionType) |
|
UpdateValueDescription(); |
|
return useShortName ? boundInputShortDescription : boundInputDescription; |
|
} |
|
|
|
bool PadmapperOptions::Action::SetValue(ControllerButtonCombo value) |
|
{ |
|
if (boundInput.button != ControllerButton_NONE) |
|
boundInput = {}; |
|
if (value.button != ControllerButton_NONE) |
|
boundInput = value; |
|
UpdateValueDescription(); |
|
return true; |
|
} |
|
|
|
void PadmapperOptions::AddAction(std::string_view key, const char *name, const char *description, ControllerButtonCombo defaultInput, std::function<void()> actionPressed, std::function<void()> actionReleased, std::function<bool()> enable, unsigned index) |
|
{ |
|
if (committed) |
|
return; |
|
actions.emplace_front(key, name, description, defaultInput, std::move(actionPressed), std::move(actionReleased), std::move(enable), index); |
|
} |
|
|
|
void PadmapperOptions::CommitActions() |
|
{ |
|
if (committed) |
|
return; |
|
actions.reverse(); |
|
committed = true; |
|
} |
|
|
|
std::string_view PadmapperOptions::InputNameForAction(std::string_view actionName, bool useShortName) const |
|
{ |
|
for (const Action &action : actions) { |
|
if (action.key == actionName && action.boundInput.button != ControllerButton_NONE) { |
|
return action.GetValueDescription(useShortName); |
|
} |
|
} |
|
return ""; |
|
} |
|
|
|
ControllerButtonCombo PadmapperOptions::ButtonComboForAction(std::string_view actionName) const |
|
{ |
|
for (const auto &action : actions) { |
|
if (action.key == actionName && action.boundInput.button != ControllerButton_NONE) { |
|
return action.boundInput; |
|
} |
|
} |
|
return ControllerButton_NONE; |
|
} |
|
|
|
const PadmapperOptions::Action *PadmapperOptions::findAction(ControllerButton button, tl::function_ref<bool(ControllerButton)> isModifierPressed) const |
|
{ |
|
// To give preference to button combinations, |
|
// first pass ignores mappings where no modifier is bound |
|
for (const Action &action : actions) { |
|
const ControllerButtonCombo combo = action.boundInput; |
|
if (combo.modifier == ControllerButton_NONE) |
|
continue; |
|
if (button != combo.button) |
|
continue; |
|
if (!isModifierPressed(combo.modifier)) |
|
continue; |
|
if (action.enable && !action.enable()) |
|
continue; |
|
return &action; |
|
} |
|
|
|
for (const Action &action : actions) { |
|
const ControllerButtonCombo combo = action.boundInput; |
|
if (combo.modifier != ControllerButton_NONE) |
|
continue; |
|
if (button != combo.button) |
|
continue; |
|
if (action.enable && !action.enable()) |
|
continue; |
|
return &action; |
|
} |
|
|
|
return nullptr; |
|
} |
|
|
|
ModOptions::ModOptions() |
|
: OptionCategoryBase("Mods", N_("Mods"), N_("Mod Settings")) |
|
{ |
|
} |
|
|
|
std::vector<std::string_view> ModOptions::GetActiveModList() |
|
{ |
|
std::vector<std::string_view> modList; |
|
for (auto &modEntry : GetModEntries()) { |
|
if (*modEntry.enabled) |
|
modList.emplace_back(modEntry.name); |
|
} |
|
return modList; |
|
} |
|
|
|
std::vector<std::string_view> ModOptions::GetModList() |
|
{ |
|
std::vector<std::string_view> modList; |
|
for (auto &modEntry : GetModEntries()) { |
|
modList.emplace_back(modEntry.name); |
|
} |
|
return modList; |
|
} |
|
|
|
std::vector<OptionEntryBase *> ModOptions::GetEntries() |
|
{ |
|
std::vector<OptionEntryBase *> optionEntries; |
|
for (auto &modEntry : GetModEntries()) { |
|
optionEntries.emplace_back(&modEntry.enabled); |
|
} |
|
return optionEntries; |
|
} |
|
|
|
void ModOptions::AddModEntry(const std::string &modName) |
|
{ |
|
auto &entries = GetModEntries(); |
|
entries.emplace_front(modName); |
|
} |
|
|
|
void ModOptions::RemoveModEntry(const std::string &modName) |
|
{ |
|
if (!modEntries) { |
|
return; |
|
} |
|
|
|
auto &entries = *modEntries; |
|
entries.remove_if([&](const ModEntry &entry) { |
|
return entry.name == modName; |
|
}); |
|
} |
|
|
|
void ModOptions::SetHellfireEnabled(bool enableHellfire) |
|
{ |
|
for (auto &modEntry : GetModEntries()) { |
|
if (modEntry.name == "Hellfire") { |
|
modEntry.enabled.SetValue(enableHellfire); |
|
break; |
|
} |
|
} |
|
} |
|
|
|
std::forward_list<ModOptions::ModEntry> &ModOptions::GetModEntries() |
|
{ |
|
if (modEntries) |
|
return *modEntries; |
|
|
|
const std::vector<std::string> modNames = ini->getKeys(key); |
|
|
|
std::forward_list<ModOptions::ModEntry> &newModEntries = modEntries.emplace(); |
|
for (auto &modName : modNames) { |
|
newModEntries.emplace_front(modName); |
|
} |
|
newModEntries.reverse(); |
|
return newModEntries; |
|
} |
|
|
|
ModOptions::ModEntry::ModEntry(std::string_view name) |
|
: name(name) |
|
, enabled(this->name, OptionEntryFlags::RecreateUI, this->name.c_str(), "", false) |
|
{ |
|
} |
|
|
|
namespace { |
|
#ifdef DEVILUTIONX_RESAMPLER_SPEEX |
|
constexpr char ResamplerSpeex[] = "Speex"; |
|
#endif |
|
#ifdef DVL_AULIB_SUPPORTS_SDL_RESAMPLER |
|
constexpr char ResamplerSDL[] = "SDL"; |
|
#endif |
|
} // namespace |
|
|
|
std::string_view ResamplerToString(Resampler resampler) |
|
{ |
|
switch (resampler) { |
|
#ifdef DEVILUTIONX_RESAMPLER_SPEEX |
|
case Resampler::Speex: |
|
return ResamplerSpeex; |
|
#endif |
|
#ifdef DVL_AULIB_SUPPORTS_SDL_RESAMPLER |
|
case Resampler::SDL: |
|
return ResamplerSDL; |
|
#endif |
|
default: |
|
return ""; |
|
} |
|
} |
|
|
|
std::optional<Resampler> ResamplerFromString(std::string_view resampler) |
|
{ |
|
#ifdef DEVILUTIONX_RESAMPLER_SPEEX |
|
if (resampler == ResamplerSpeex) |
|
return Resampler::Speex; |
|
#endif |
|
#ifdef DVL_AULIB_SUPPORTS_SDL_RESAMPLER |
|
if (resampler == ResamplerSDL) |
|
return Resampler::SDL; |
|
#endif |
|
return std::nullopt; |
|
} |
|
|
|
} // namespace devilution
|
|
|