Files
DevilutionX/Source/options.cpp
T
2026-05-21 05:39:30 +02:00

1683 lines
60 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @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) || defined(__EMSCRIPTEN__)
// See https://github.com/diasurgical/devilutionX/issues/2502
// Emscripten: Software cursor works better in browsers
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."),
#ifdef __EMSCRIPTEN__
false // Default to windowed mode for browser
#else
true
#endif
)
#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."),
#if defined(__DJGPP__) || defined(__EMSCRIPTEN__)
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)
, colorCycling("Color Cycling", OptionEntryFlags::None, N_("Color Cycling"), N_("Color cycling effect used for water, lava, and acid animation."), true)
, alternateNestArt("Alternate nest art", OptionEntryFlags::OnlyHellfire | OptionEntryFlags::CantChangeInGame, N_("Alternate nest art"), N_("The game will use an alternative palette for Hellfires 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
, showFPS("Show FPS", OptionEntryFlags::None, N_("Show FPS"), N_("Displays the FPS in the upper left corner of the screen."), false)
{
}
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 })
, visualStoreUI("Visual Store UI", OptionEntryFlags::None, N_("Visual Store UI"), N_("Use visual grid-based store interface instead of text-based menus. Both store and inventory panels open together."), false)
, 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,
&visualStoreUI,
&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