diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 22616bc45..38cd816e8 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -7,7 +7,7 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ && apt-get -y install tar curl zip unzip bash-completion build-essential ripgrep htop \ ninja-build ccache g++ mold gdb clang-format clang-tidy \ rpm pkg-config cmake git smpq gettext libsdl2-dev libsdl2-image-dev libsodium-dev \ - libpng-dev libbz2-dev libfmt-dev libgtest-dev libgmock-dev libbenchmark-dev libsimpleini-dev zsh \ + libpng-dev libbz2-dev libfmt-dev libgtest-dev libgmock-dev libbenchmark-dev zsh \ qtbase5-dev qt6-base-dev ristretto \ && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* diff --git a/.github/workflows/Linux_aarch64.yml b/.github/workflows/Linux_aarch64.yml index 7c6bcb52f..7470f8bdc 100644 --- a/.github/workflows/Linux_aarch64.yml +++ b/.github/workflows/Linux_aarch64.yml @@ -74,12 +74,11 @@ jobs: CMAKE_BUILD_TYPE: ${{github.event_name == 'release' && 'Release' || 'RelWithDebInfo'}} # We set DEVILUTIONX_SYSTEM_LIBFMT=OFF because its soversion changes frequently. # We set DEVILUTIONX_SYSTEM_LIBSODIUM=OFF because its soversion changes frequently. - # We set DEVILUTIONX_SYSTEM_SIMPLEINI=OFF because we require v4.19+, still missing from many distributions. # We set DEVILUTIONX_SYSTEM_BZIP2=OFF because Fedora and Debian do not agree on how to link it. run: | cmake -S. -Bbuild -DCMAKE_TOOLCHAIN_FILE=../CMake/platforms/aarch64-linux-gnu-clang-static-libc++.toolchain.cmake \ -DCMAKE_BUILD_TYPE=${{env.CMAKE_BUILD_TYPE}} -DCMAKE_INSTALL_PREFIX=/usr -DCPACK=ON -DDEVILUTIONX_SYSTEM_LIBFMT=OFF \ - -DDEVILUTIONX_SYSTEM_LIBSODIUM=OFF -DDEVILUTIONX_SYSTEM_SIMPLEINI=OFF -DDEVILUTIONX_SYSTEM_BZIP2=OFF && \ + -DDEVILUTIONX_SYSTEM_LIBSODIUM=OFF -DDEVILUTIONX_SYSTEM_BZIP2=OFF && \ cmake --build build -j $(getconf _NPROCESSORS_ONLN) --target package - name: Package diff --git a/.github/workflows/Linux_x86.yml b/.github/workflows/Linux_x86.yml index 3802710ea..8a350cb50 100644 --- a/.github/workflows/Linux_x86.yml +++ b/.github/workflows/Linux_x86.yml @@ -65,14 +65,12 @@ jobs: CMAKE_BUILD_TYPE: ${{github.event_name == 'release' && 'Release' || 'RelWithDebInfo'}} # We set DEVILUTIONX_SYSTEM_LIBFMT=OFF because its soversion changes frequently. # We set DEVILUTIONX_SYSTEM_LIBSODIUM=OFF because its soversion changes frequently. - # We set DEVILUTIONX_SYSTEM_SIMPLEINI=OFF because we require v4.19+, still missing from many distributions, # We set DEVILUTIONX_SYSTEM_BZIP2=OFF because Fedora and Debian do not agree on how to link it. - # and there is no libsimpleini-dev:i386 for Ubuntu 20.04. run: | cmake -S. -Bbuild -DCMAKE_TOOLCHAIN_FILE=../CMake/platforms/linux_i386.toolchain.cmake \ -DCMAKE_BUILD_TYPE=${{env.CMAKE_BUILD_TYPE}} -DCMAKE_INSTALL_PREFIX=/usr -DCPACK=ON \ -DBUILD_TESTING=OFF -DDEVILUTIONX_SYSTEM_LIBFMT=OFF -DDEVILUTIONX_SYSTEM_LIBSODIUM=OFF \ - -DDEVILUTIONX_SYSTEM_SIMPLEINI=OFF -DDEVILUTIONX_SYSTEM_BZIP2=OFF && \ + -DDEVILUTIONX_SYSTEM_BZIP2=OFF && \ cmake --build build -j $(getconf _NPROCESSORS_ONLN) --target package - name: Package diff --git a/.github/workflows/Linux_x86_64.yml b/.github/workflows/Linux_x86_64.yml index 3c322bbfa..e34989409 100644 --- a/.github/workflows/Linux_x86_64.yml +++ b/.github/workflows/Linux_x86_64.yml @@ -63,12 +63,11 @@ jobs: CMAKE_BUILD_TYPE: ${{github.event_name == 'release' && 'Release' || 'RelWithDebInfo'}} # We set DEVILUTIONX_SYSTEM_LIBFMT=OFF because its soversion changes frequently. # We set DEVILUTIONX_SYSTEM_LIBSODIUM=OFF because its soversion changes frequently. - # We set DEVILUTIONX_SYSTEM_SIMPLEINI=OFF because we require v4.19+, still missing from many distributions. # We set DEVILUTIONX_SYSTEM_BZIP2=OFF because Fedora and Debian do not agree on how to link it. run: | cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=${{env.CMAKE_BUILD_TYPE}} -DCMAKE_INSTALL_PREFIX=/usr -DCPACK=ON \ -DDISCORD_INTEGRATION=ON -DBUILD_TESTING=OFF -DDEVILUTIONX_SYSTEM_LIBFMT=OFF \ - -DDEVILUTIONX_SYSTEM_LIBSODIUM=OFF -DDEVILUTIONX_SYSTEM_SIMPLEINI=OFF -DDEVILUTIONX_SYSTEM_BZIP2=OFF && \ + -DDEVILUTIONX_SYSTEM_LIBSODIUM=OFF -DDEVILUTIONX_SYSTEM_BZIP2=OFF && \ cmake --build build -j $(getconf _NPROCESSORS_ONLN) --target package - name: Package diff --git a/3rdParty/simpleini/CMakeLists.txt b/3rdParty/simpleini/CMakeLists.txt deleted file mode 100644 index e32fcf74d..000000000 --- a/3rdParty/simpleini/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ -include(functions/FetchContent_MakeAvailableExcludeFromAll) - -include(FetchContent) -FetchContent_Declare(simpleini - URL https://github.com/brofield/simpleini/archive/56499b5af5d2195c6acfc58c4630b70e0c9c4c21.tar.gz - URL_HASH MD5=02a561cea03ea11acb65848318ec4a81 -) -FetchContent_MakeAvailableExcludeFromAll(simpleini) - -add_library(simpleini INTERFACE) -target_include_directories(simpleini INTERFACE ${simpleini_SOURCE_DIR}) -add_library(simpleini::simpleini ALIAS simpleini) diff --git a/CMake/Dependencies.cmake b/CMake/Dependencies.cmake index cd71bd863..4240de6ef 100644 --- a/CMake/Dependencies.cmake +++ b/CMake/Dependencies.cmake @@ -201,22 +201,6 @@ if(WIN32 AND NOT UWP_LIB) add_subdirectory(3rdParty/find_steam_game) endif() -if(NOT DEFINED DEVILUTIONX_SYSTEM_SIMPLEINI) - find_package(simpleini 4.19 QUIET) - if(simpleini_FOUND) - message("-- Found simpleini") - else() - message("-- Suitable system simpleini package not found, will use simpleini from source") - set(DEVILUTIONX_SYSTEM_SIMPLEINI OFF) - endif() -endif() -dependency_options("simpleini" DEVILUTIONX_SYSTEM_SIMPLEINI ON DEVILUTIONX_STATIC_SIMPLEINI) -if(DEVILUTIONX_SYSTEM_SIMPLEINI) - find_package(simpleini 4.19 REQUIRED) -else() - add_subdirectory(3rdParty/simpleini) -endif() - if(SUPPORTS_MPQ) add_subdirectory(3rdParty/libmpq) endif() diff --git a/Packaging/nix/debian-cross-aarch64-prep.sh b/Packaging/nix/debian-cross-aarch64-prep.sh index 22704264c..7619a971b 100755 --- a/Packaging/nix/debian-cross-aarch64-prep.sh +++ b/Packaging/nix/debian-cross-aarch64-prep.sh @@ -22,7 +22,7 @@ fi PACKAGES=( cmake git smpq gettext dpkg-cross libc-dev-arm64-cross libsdl2-dev:arm64 libsdl2-image-dev:arm64 libsodium-dev:arm64 - libsimpleini-dev:arm64 libpng-dev:arm64 libbz2-dev:arm64 libfmt-dev:arm64 + libpng-dev:arm64 libbz2-dev:arm64 libfmt-dev:arm64 libspeechd-dev:arm64 ) diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index f29184104..317b859dc 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -367,6 +367,17 @@ target_link_libraries(libdevilutionx_format_int PUBLIC libdevilutionx_strings ) +add_devilutionx_object_library(libdevilutionx_ini + utils/ini.cpp +) +target_link_libraries(libdevilutionx_ini PUBLIC + fmt::fmt + tl + unordered_dense::unordered_dense + libdevilutionx_strings + libdevilutionx_utf8 +) + add_library(libdevilutionx_log INTERFACE) target_include_directories(libdevilutionx_log INTERFACE ${PROJECT_SOURCE_DIR}/Source) @@ -402,13 +413,13 @@ target_link_libraries(libdevilutionx PUBLIC DevilutionX::SDL fmt::fmt libsmackerdec - simpleini::simpleini tl unordered_dense::unordered_dense libdevilutionx_codec libdevilutionx_crawl libdevilutionx_format_int libdevilutionx_file_util + libdevilutionx_ini libdevilutionx_parse_int libdevilutionx_strings libdevilutionx_utf8 diff --git a/Source/options.cpp b/Source/options.cpp index 84b7fee86..06e52eedd 100644 --- a/Source/options.cpp +++ b/Source/options.cpp @@ -5,14 +5,15 @@ */ #include +#include #include #include +#include +#include +#include #include -#define SI_NO_CONVERSION -#include - #include "control.h" #include "controls/controller.h" #include "controls/game_controls.h" @@ -28,8 +29,10 @@ #include "utils/algorithm/container.hpp" #include "utils/display.h" #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/str_cat.hpp" #include "utils/str_split.hpp" @@ -58,6 +61,8 @@ namespace devilution { namespace { +std::optional ini; + #if defined(__ANDROID__) || (defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE == 1) constexpr OptionEntryFlags OnlyIfSupportsWindowed = OptionEntryFlags::Invisible; #else @@ -79,167 +84,45 @@ std::string GetIniPath() return path; } -CSimpleIni &GetIni() +void LoadIni() { - static CSimpleIni ini; - static bool isIniLoaded = false; - if (!isIniLoaded) { - auto path = GetIniPath(); - FILE *file = OpenFile(path.c_str(), "rb"); - ini.SetSpaces(false); - ini.SetMultiKey(); - if (file != nullptr) { - ini.LoadFile(file); - std::fclose(file); - } - isIniLoaded = true; - } - return ini; -} - -bool IniChanged = false; - -/** - * @brief Checks if a ini entry is changed by comparing value before and after - */ -class IniChangedChecker { -public: - IniChangedChecker(const char *sectionName, const char *keyName) - { - this->sectionName_ = sectionName; - this->keyName_ = keyName; - oldValue_ = GetValue(); - if (!oldValue_) { - // No entry found in original ini => new entry => changed - IniChanged = true; - } - } - ~IniChangedChecker() - { - auto newValue = GetValue(); - if (oldValue_ != newValue) - IniChanged = true; - } - -private: - std::optional GetValue() - { - std::list values; - if (!GetIni().GetAllValues(sectionName_, keyName_, values)) - return std::nullopt; - std::string ret; - for (auto &entry : values) { - if (entry.pItem != nullptr) - ret.append(entry.pItem); - ret.append("\n"); + std::vector 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(size); + if (std::fread(buffer.data(), size, 1, file) != 1) { + const char *errorMessage = std::strerror(errno); + if (errorMessage == nullptr) errorMessage = ""; + LogError(LogCategory::System, "std::fread: failed with \"{}\"", errorMessage); + buffer.clear(); + } } - return ret; - } - - std::optional oldValue_; - const char *sectionName_; - const char *keyName_; -}; - -int GetIniInt(const char *keyname, const char *valuename, int defaultValue) -{ - return GetIni().GetLongValue(keyname, valuename, defaultValue); -} - -bool GetIniBool(const char *sectionName, const char *keyName, bool defaultValue) -{ - return GetIni().GetBoolValue(sectionName, keyName, defaultValue); -} - -float GetIniFloat(const char *sectionName, const char *keyName, float defaultValue) -{ - return (float)GetIni().GetDoubleValue(sectionName, keyName, defaultValue); -} - -bool GetIniValue(std::string_view sectionName, std::string_view keyName, char *string, size_t stringSize, const char *defaultString = "") -{ - std::string sectionNameStr { sectionName }; - std::string keyNameStr { keyName }; - const char *value = GetIni().GetValue(sectionNameStr.c_str(), keyNameStr.c_str()); - if (value == nullptr) { - CopyUtf8(string, defaultString, stringSize); - return false; - } - CopyUtf8(string, value, stringSize); - return true; -} - -bool GetIniStringVector(const char *sectionName, const char *keyName, std::vector &stringValues) -{ - std::list values; - if (!GetIni().GetAllValues(sectionName, keyName, values)) { - return false; - } - for (auto &entry : values) { - stringValues.emplace_back(entry.pItem); - } - return true; -} - -void SetIniValue(const char *keyname, const char *valuename, int value) -{ - IniChangedChecker changedChecker(keyname, valuename); - GetIni().SetLongValue(keyname, valuename, value, nullptr, false, true); -} - -void SetIniValue(const char *keyname, const char *valuename, bool value) -{ - IniChangedChecker changedChecker(keyname, valuename); - GetIni().SetLongValue(keyname, valuename, value ? 1 : 0, nullptr, false, true); -} - -void SetIniValue(const char *keyname, const char *valuename, float value) -{ - IniChangedChecker changedChecker(keyname, valuename); - GetIni().SetDoubleValue(keyname, valuename, value, nullptr, true); -} - -void SetIniValue(const char *sectionName, const char *keyName, const char *value) -{ - IniChangedChecker changedChecker(sectionName, keyName); - auto &ini = GetIni(); - ini.SetValue(sectionName, keyName, value, nullptr, true); -} - -void SetIniValue(std::string_view sectionName, std::string_view keyName, std::string_view value) -{ - std::string sectionNameStr { sectionName }; - std::string keyNameStr { keyName }; - std::string valueStr { value }; - SetIniValue(sectionNameStr.c_str(), keyNameStr.c_str(), valueStr.c_str()); -} - -void SetIniValue(const char *keyname, const char *valuename, const std::vector &stringValues) -{ - IniChangedChecker changedChecker(keyname, valuename); - bool firstSet = true; - for (auto &value : stringValues) { - GetIni().SetValue(keyname, valuename, value.c_str(), nullptr, firstSet); - firstSet = false; + std::fclose(file); } - if (firstSet) - GetIni().SetValue(keyname, valuename, "", nullptr, true); + tl::expected 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 (!IniChanged) - return; + if (!ini.has_value()) return; + if (!ini->changed()) return; RecursivelyCreateDir(paths::ConfigPath().c_str()); const std::string iniPath = GetIniPath(); - FILE *file = OpenFile(iniPath.c_str(), "wb"); - if (file != nullptr) { - GetIni().SaveFile(file, true); - std::fclose(file); - } else { - LogError("Failed to write ini file to {}: {}", iniPath, std::strerror(errno)); + LoggedFStream out; + if (!out.Open(iniPath.c_str(), "wb")) { + LogError("Failed to open ini file for writing at {}: {}", iniPath, std::strerror(errno)); + return; } - IniChanged = false; + const std::string newContents = ini->serialize(); + if (out.Write(newContents.data(), newContents.size())) { + ini->markAsUnchanged(); + } + out.Close(); } #if SDL_VERSION_ATLEAST(2, 0, 0) @@ -353,25 +236,32 @@ bool HardwareCursorSupported() void LoadOptions() { + LoadIni(); for (OptionCategoryBase *pCategory : sgOptions.GetCategories()) { for (OptionEntryBase *pEntry : pCategory->GetEntries()) { pEntry->LoadFromIni(pCategory->GetKey()); } } - GetIniValue("Hellfire", "SItem", sgOptions.Hellfire.szItem, sizeof(sgOptions.Hellfire.szItem), ""); - - GetIniValue("Network", "Bind Address", sgOptions.Network.szBindAddress, sizeof(sgOptions.Network.szBindAddress), "0.0.0.0"); - GetIniValue("Network", "Previous Game ID", sgOptions.Network.szPreviousZTGame, sizeof(sgOptions.Network.szPreviousZTGame), ""); - GetIniValue("Network", "Previous Host", sgOptions.Network.szPreviousHost, sizeof(sgOptions.Network.szPreviousHost), ""); + ini->getUtf8Buf("Hellfire", "SItem", sgOptions.Hellfire.szItem, sizeof(sgOptions.Hellfire.szItem)); + ini->getUtf8Buf("Network", "Bind Address", "0.0.0.0", sgOptions.Network.szBindAddress, sizeof(sgOptions.Network.szBindAddress)); + ini->getUtf8Buf("Network", "Previous Game ID", sgOptions.Network.szPreviousZTGame, sizeof(sgOptions.Network.szPreviousZTGame)); + ini->getUtf8Buf("Network", "Previous Host", sgOptions.Network.szPreviousHost, sizeof(sgOptions.Network.szPreviousHost)); - for (size_t i = 0; i < QUICK_MESSAGE_OPTIONS; i++) - GetIniStringVector("NetMsg", QuickMessages[i].key, sgOptions.Chat.szHotKeyMsgs[i]); + for (size_t i = 0; i < QUICK_MESSAGE_OPTIONS; i++) { + std::span values = ini->get("NetMsg", QuickMessages[i].key); + std::vector &result = sgOptions.Chat.szHotKeyMsgs[i]; + result.clear(); + result.reserve(values.size()); + for (const Ini::Value &value : values) { + result.emplace_back(value.value); + } + } - GetIniValue("Controller", "Mapping", sgOptions.Controller.szMapping, sizeof(sgOptions.Controller.szMapping), ""); - sgOptions.Controller.fDeadzone = GetIniFloat("Controller", "deadzone", 0.07F); + ini->getUtf8Buf("Controller", "Mapping", sgOptions.Controller.szMapping, sizeof(sgOptions.Controller.szMapping)); + sgOptions.Controller.fDeadzone = ini->getFloat("Controller", "deadzone", 0.07F); #ifdef __vita__ - sgOptions.Controller.bRearTouch = GetIniBool("Controller", "Enable Rear Touchpad", true); + sgOptions.Controller.bRearTouch = ini->getBool("Controller", "Enable Rear Touchpad", true); #endif if (demo::IsRunning()) @@ -389,19 +279,20 @@ void SaveOptions() } } - SetIniValue("Hellfire", "SItem", sgOptions.Hellfire.szItem); + ini->set("Hellfire", "SItem", sgOptions.Hellfire.szItem); - SetIniValue("Network", "Bind Address", sgOptions.Network.szBindAddress); - SetIniValue("Network", "Previous Game ID", sgOptions.Network.szPreviousZTGame); - SetIniValue("Network", "Previous Host", sgOptions.Network.szPreviousHost); + ini->set("Network", "Bind Address", sgOptions.Network.szBindAddress); + ini->set("Network", "Previous Game ID", sgOptions.Network.szPreviousZTGame); + ini->set("Network", "Previous Host", sgOptions.Network.szPreviousHost); - for (size_t i = 0; i < QUICK_MESSAGE_OPTIONS; i++) - SetIniValue("NetMsg", QuickMessages[i].key, sgOptions.Chat.szHotKeyMsgs[i]); + for (size_t i = 0; i < QUICK_MESSAGE_OPTIONS; i++) { + ini->set("NetMsg", QuickMessages[i].key, sgOptions.Chat.szHotKeyMsgs[i]); + } - SetIniValue("Controller", "Mapping", sgOptions.Controller.szMapping); - SetIniValue("Controller", "deadzone", sgOptions.Controller.fDeadzone); + ini->set("Controller", "Mapping", sgOptions.Controller.szMapping); + ini->set("Controller", "deadzone", sgOptions.Controller.fDeadzone); #ifdef __vita__ - SetIniValue("Controller", "Enable Rear Touchpad", sgOptions.Controller.bRearTouch); + ini->set("Controller", "Enable Rear Touchpad", sgOptions.Controller.bRearTouch); #endif SaveIni(); @@ -431,11 +322,11 @@ void OptionEntryBase::NotifyValueChanged() void OptionEntryBoolean::LoadFromIni(std::string_view category) { - value = GetIniBool(category.data(), key.data(), defaultValue); + value = ini->getBool(category, key, defaultValue); } void OptionEntryBoolean::SaveToIni(std::string_view category) const { - SetIniValue(category.data(), key.data(), value); + ini->set(category, key, value); } void OptionEntryBoolean::SetValue(bool value) { @@ -462,11 +353,11 @@ std::string_view OptionEntryListBase::GetValueDescription() const void OptionEntryEnumBase::LoadFromIni(std::string_view category) { - value = GetIniInt(category.data(), key.data(), defaultValue); + value = ini->getInt(category, key, defaultValue); } void OptionEntryEnumBase::SaveToIni(std::string_view category) const { - SetIniValue(category.data(), key.data(), value); + ini->set(category, key, value); } void OptionEntryEnumBase::SetValueInternal(int value) { @@ -501,7 +392,7 @@ void OptionEntryEnumBase::SetActiveListIndex(size_t index) void OptionEntryIntBase::LoadFromIni(std::string_view category) { - value = GetIniInt(category.data(), key.data(), defaultValue); + value = ini->getInt(category, key, defaultValue); if (c_find(entryValues, value) == entryValues.end()) { entryValues.insert(c_lower_bound(entryValues, value), value); entryNames.clear(); @@ -509,7 +400,7 @@ void OptionEntryIntBase::LoadFromIni(std::string_view category) } void OptionEntryIntBase::SaveToIni(std::string_view category) const { - SetIniValue(category.data(), key.data(), value); + ini->set(category, key, value); } void OptionEntryIntBase::SetValueInternal(int value) { @@ -675,12 +566,12 @@ OptionEntryResolution::OptionEntryResolution() } void OptionEntryResolution::LoadFromIni(std::string_view category) { - size = { GetIniInt(category.data(), "Width", DEFAULT_WIDTH), GetIniInt(category.data(), "Height", DEFAULT_HEIGHT) }; + size = { ini->getInt(category, "Width", DEFAULT_WIDTH), ini->getInt(category, "Height", DEFAULT_HEIGHT) }; } void OptionEntryResolution::SaveToIni(std::string_view category) const { - SetIniValue(category.data(), "Width", size.width); - SetIniValue(category.data(), "Height", size.height); + ini->set(category, "Width", size.width); + ini->set(category, "Height", size.height); } void OptionEntryResolution::InvalidateList() @@ -821,8 +712,8 @@ OptionEntryResampler::OptionEntryResampler() } void OptionEntryResampler::LoadFromIni(std::string_view category) { - char resamplerStr[32]; - if (GetIniValue(category, key, resamplerStr, sizeof(resamplerStr))) { + std::string_view resamplerStr = ini->getString(category, key); + if (!resamplerStr.empty()) { std::optional resampler = ResamplerFromString(resamplerStr); if (resampler) { resampler_ = *resampler; @@ -836,7 +727,7 @@ void OptionEntryResampler::LoadFromIni(std::string_view category) void OptionEntryResampler::SaveToIni(std::string_view category) const { - SetIniValue(category, key, ResamplerToString(resampler_)); + ini->set(category, key, ResamplerToString(resampler_)); } size_t OptionEntryResampler::GetListSize() const @@ -878,15 +769,13 @@ OptionEntryAudioDevice::OptionEntryAudioDevice() } void OptionEntryAudioDevice::LoadFromIni(std::string_view category) { - char deviceStr[100]; - GetIniValue(category, key, deviceStr, sizeof(deviceStr), ""); - deviceName_ = deviceStr; + deviceName_ = ini->getString(category, key); } void OptionEntryAudioDevice::SaveToIni(std::string_view category) const { #if SDL_VERSION_ATLEAST(2, 0, 0) - SetIniValue(category, key, deviceName_); + ini->set(category, key, deviceName_); #endif } @@ -1162,11 +1051,10 @@ OptionEntryLanguageCode::OptionEntryLanguageCode() } void OptionEntryLanguageCode::LoadFromIni(std::string_view category) { - if (GetIniValue(category, key, szCode, sizeof(szCode))) { - if (HasTranslation(szCode)) { - // User preferred language is available - return; - } + 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 @@ -1205,7 +1093,7 @@ void OptionEntryLanguageCode::LoadFromIni(std::string_view category) } void OptionEntryLanguageCode::SaveToIni(std::string_view category) const { - SetIniValue(category, key, szCode); + ini->set(category, key, szCode); } void OptionEntryLanguageCode::CheckLanguagesAreInitialized() const @@ -1391,22 +1279,22 @@ std::string_view KeymapperOptions::Action::GetName() const void KeymapperOptions::Action::LoadFromIni(std::string_view category) { - std::array result; - if (!GetIniValue(category.data(), key.data(), result.data(), result.size())) { + const std::span iniValues = ini->get(category, key); + if (iniValues.empty()) { SetValue(defaultKey); return; // Use the default key if no key has been set. } - std::string readKey = result.data(); - if (readKey.empty()) { + const std::string_view iniValue = iniValues.back().value; + if (iniValue.empty()) { SetValue(SDLK_UNKNOWN); return; } - auto keyIt = sgOptions.Keymapper.keyNameToKeyID.find(readKey); + auto keyIt = sgOptions.Keymapper.keyNameToKeyID.find(iniValue); if (keyIt == sgOptions.Keymapper.keyNameToKeyID.end()) { // Use the default key if the key is unknown. - Log("Keymapper: unknown key '{}'", readKey); + Log("Keymapper: unknown key '{}'", iniValue); SetValue(defaultKey); return; } @@ -1419,7 +1307,7 @@ void KeymapperOptions::Action::SaveToIni(std::string_view category) const { if (boundKey == SDLK_UNKNOWN) { // Just add an empty config entry if the action is unbound. - SetIniValue(category.data(), key.data(), ""); + ini->set(category, key, std::string {}); return; } auto keyNameIt = sgOptions.Keymapper.keyIDToKeyName.find(boundKey); @@ -1427,7 +1315,7 @@ void KeymapperOptions::Action::SaveToIni(std::string_view category) const LogVerbose("Keymapper: no name found for key {} bound to {}", boundKey, key); return; } - SetIniValue(category.data(), key.data(), keyNameIt->second.c_str()); + ini->set(category, key, keyNameIt->second); } std::string_view KeymapperOptions::Action::GetValueDescription() const @@ -1611,15 +1499,16 @@ std::string_view PadmapperOptions::Action::GetName() const void PadmapperOptions::Action::LoadFromIni(std::string_view category) { - std::array result; - if (!GetIniValue(category.data(), key.data(), result.data(), result.size())) { + const std::span 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(result.data(), '+'); + auto parts = SplitByChar(iniValue, '+'); auto it = parts.begin(); if (it == parts.end()) { SetValue(ControllerButtonCombo {}); @@ -1660,7 +1549,7 @@ 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. - SetIniValue(category.data(), key.data(), ""); + ini->set(category, key, ""); return; } std::string inputName = sgOptions.Padmapper.buttonToButtonName[static_cast(boundInput.button)]; @@ -1676,7 +1565,7 @@ void PadmapperOptions::Action::SaveToIni(std::string_view category) const } inputName = StrCat(modifierName, "+", inputName); } - SetIniValue(category.data(), key.data(), inputName.data()); + ini->set(category, key, inputName.data()); } void PadmapperOptions::Action::UpdateValueDescription() const diff --git a/Source/options.h b/Source/options.h index 3c9125735..3ae6c1f51 100644 --- a/Source/options.h +++ b/Source/options.h @@ -16,6 +16,7 @@ #include "engine/sound_defs.hpp" #include "pack.h" #include "utils/enum_traits.h" +#include "utils/string_view_hash.hpp" namespace devilution { @@ -710,7 +711,7 @@ private: std::forward_list actions; ankerl::unordered_dense::segmented_map> keyIDToAction; ankerl::unordered_dense::segmented_map keyIDToKeyName; - ankerl::unordered_dense::segmented_map keyNameToKeyID; + ankerl::unordered_dense::segmented_map keyNameToKeyID; }; /** The Padmapper maps gamepad buttons to actions. */ @@ -782,7 +783,7 @@ private: std::forward_list actions; std::array::value> buttonToReleaseAction; std::array::value> buttonToButtonName; - ankerl::unordered_dense::segmented_map buttonNameToButton; + ankerl::unordered_dense::segmented_map buttonNameToButton; bool committed = false; const Action *FindAction(ControllerButton button) const; diff --git a/Source/utils/ini.cpp b/Source/utils/ini.cpp new file mode 100644 index 000000000..35d39afdc --- /dev/null +++ b/Source/utils/ini.cpp @@ -0,0 +1,415 @@ +#include "utils/ini.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "utils/algorithm/container.hpp" +#include "utils/str_cat.hpp" +#include "utils/string_view_hash.hpp" +#include "utils/utf8.hpp" + +namespace devilution { + +// We avoid including the "appfat.h" to avoid depending on SDL in tests. +[[noreturn]] extern void app_fatal(std::string_view str); + +namespace { + +// Returns a pointer to the first non-leading whitespace. +// Only ' ' and '\t' are considered whitespace. +// Requires: begin <= end. +const char *SkipLeadingWhitespace(const char *begin, const char *end) +{ + while (begin != end && (*begin == ' ' || *begin == '\t')) { + ++begin; + } + return begin; +} + +// Returns a pointer to the last non-whitespace. +// Only ' ' and '\t' are considered whitespace. +// Requires: begin <= end. +const char *SkipTrailingWhitespace(const char *begin, const char *end) +{ + while (begin != end && (*(end - 1) == ' ' || *(end - 1) == '\t')) { + --end; + } + return end; +} + +// Skips UTF-8 byte order mark. +// See https://en.wikipedia.org/wiki/Byte_order_mark +const char *SkipUtf8Bom(const char *begin, const char *end) +{ + if (end - begin >= 3 && begin[0] == '\xEF' && begin[1] == '\xBB' && begin[2] == '\xBF') { + return begin + 3; + } + return begin; +} + +} // namespace + +Ini::Values::Values() + : rep_(std::vector {}) +{ +} + +Ini::Values::Values(const Value &data) + : rep_(data) +{ +} + +std::span Ini::Values::get() const +{ + if (std::holds_alternative(rep_)) { + return { &std::get(rep_), 1 }; + } + return std::get>(rep_); +} + +std::span Ini::Values::get() +{ + if (std::holds_alternative(rep_)) { + return { &std::get(rep_), 1 }; + } + return std::get>(rep_); +} + +void Ini::Values::append(const Ini::Value &value) +{ + if (std::holds_alternative(rep_)) { + rep_ = std::vector { std::get(rep_), value }; + return; + } + std::get>(rep_).push_back(value); +} + +tl::expected Ini::parse(std::string_view buffer) +{ + Ini::FileData fileData; + ankerl::unordered_dense::map *sectionEntries = nullptr; + const char *eof = buffer.data() + buffer.size(); + const char *lineBegin = SkipUtf8Bom(buffer.data(), eof); + size_t lineNum = 0; + + const char *commentBegin = nullptr; + const char *nextLineBegin; + for (; lineBegin < eof; lineBegin = nextLineBegin) { + ++lineNum; + const char *lineEnd = static_cast(memchr(lineBegin, '\n', eof - lineBegin)); + if (lineEnd == nullptr) { + lineEnd = eof; + nextLineBegin = eof; + } else { + nextLineBegin = lineEnd + 1; + if (lineBegin + 1 < lineEnd && *(lineEnd - 1) == '\r') --lineEnd; + } + const char *keyBegin = SkipLeadingWhitespace(lineBegin, lineEnd); + if (keyBegin == lineEnd) continue; + + if (*keyBegin == ';') { + if (commentBegin == nullptr) commentBegin = lineBegin; + continue; + } + std::string_view comment; + if (commentBegin != nullptr) { + comment = std::string_view { commentBegin, lineBegin }; + } + + if (*keyBegin == '[') { + const char *keyEnd = ++keyBegin; + while (keyEnd < lineEnd && *keyEnd != ']') { + ++keyEnd; + } + if (keyEnd == lineEnd) { + return tl::make_unexpected(fmt::format("line {}: unclosed section name {}", lineNum, std::string_view(keyBegin, keyEnd))); + } + if (const char *after = SkipTrailingWhitespace(keyEnd + 1, lineEnd); after != lineEnd) { + return tl::make_unexpected(fmt::format("line {}: content after section [{}]: {}", lineNum, std::string_view(keyBegin, keyEnd), std::string_view(after, lineEnd))); + } + const std::string_view sectionName = std::string_view(keyBegin, keyEnd); + auto it = fileData.sections.find(sectionName); + if (it == fileData.sections.end()) { + it = fileData.sections.emplace_hint(it, sectionName, + SectionData { + .comment = std::string(comment), + .entries = {}, + .index = static_cast(fileData.sections.size()), + }); + } + sectionEntries = &it->second.entries; + commentBegin = nullptr; + continue; + } + + if (sectionEntries == nullptr) return tl::unexpected(fmt::format("line {}: key not in any section", lineNum)); + const char *eqPos = static_cast(memchr(keyBegin, '=', lineEnd - keyBegin)); + if (eqPos == nullptr) { + return tl::make_unexpected(fmt::format("line {}: key {} has no value", lineNum, std::string_view(keyBegin, lineEnd))); + } + const std::string_view key = std::string_view(keyBegin, SkipTrailingWhitespace(keyBegin, eqPos)); + const std::string_view value = std::string_view(SkipLeadingWhitespace(eqPos + 1, lineEnd), lineEnd); + if (const auto it = sectionEntries->find(key); it != sectionEntries->end()) { + it->second.values.append(Value { std::string(comment), std::string(value) }); + } else { + sectionEntries->emplace_hint(it, key, + ValuesData { + .values = Values { Value { + .comment = std::string(comment), + .value = std::string(value), + } }, + .index = static_cast(sectionEntries->size()), + }); + } + commentBegin = nullptr; + } + return Ini(std::move(fileData)); +} + +std::span Ini::get(std::string_view section, std::string_view key) const +{ + const auto sectionIt = data_.sections.find(section); + if (sectionIt == data_.sections.end()) return {}; + const auto it = sectionIt->second.entries.find(key); + if (it == sectionIt->second.entries.end()) return {}; + return it->second.values.get(); +} + +std::string_view Ini::getString(std::string_view section, std::string_view key, std::string_view defaultValue) const +{ + const std::span xs = get(section, key); + if (xs.empty() || xs.back().value.empty()) return defaultValue; + return xs.back().value; +} + +int Ini::getInt(std::string_view section, std::string_view key, int defaultValue) const +{ + const std::span xs = get(section, key); + if (xs.empty() || xs.back().value.empty()) return defaultValue; + const std::string_view str = xs.back().value; + int value; + const std::from_chars_result result = std::from_chars(str.data(), str.data() + str.size(), value); + if (result.ec != std::errc()) { + app_fatal(fmt::format("ini: Failed to parse {}.{}={} as int", section, key, str)); + return defaultValue; + } + return value; +} + +bool Ini::getBool(std::string_view section, std::string_view key, bool defaultValue) const +{ + const std::span xs = get(section, key); + if (xs.empty() || xs.back().value.empty()) return defaultValue; + const std::string_view str = xs.back().value; + if (str == "0") return false; + if (str == "1") return true; + app_fatal(fmt::format("ini: Failed to parse {}.{}={} as bool", section, key, str)); +} + +float Ini::getFloat(std::string_view section, std::string_view key, float defaultValue) const +{ + const std::span xs = get(section, key); + if (xs.empty() || xs.back().value.empty()) return defaultValue; + const std::string_view str = xs.back().value; + float value; + const std::from_chars_result result = std::from_chars(str.data(), str.data() + str.size(), value); + if (result.ec != std::errc()) { + app_fatal(fmt::format("ini: Failed to parse {}.{}={} as float", section, key, str)); + return defaultValue; + } + return value; +} + +void Ini::getUtf8Buf(std::string_view section, std::string_view key, std::string_view defaultValue, char *dst, size_t dstSize) const +{ + CopyUtf8(dst, getString(section, key, defaultValue), dstSize); +} + +void Ini::set(std::string_view section, std::string_view key, Ini::Values &&values) +{ + const std::span updated = values.get(); + + auto sectionIt = data_.sections.find(section); + if (sectionIt == data_.sections.end()) { + // Deleting a key from a non-existing section + if (updated.empty()) return; + + // Adding a new section and key + data_.sections.emplace_hint(sectionIt, section, + SectionData { + .comment = {}, + .entries = { { std::string(key), ValuesData { .values = std::move(values), .index = 0 } } }, + .index = static_cast(data_.sections.size()), + }); + changed_ = true; + return; + } + const auto it = sectionIt->second.entries.find(key); + if (it == sectionIt->second.entries.end()) { + // Deleting a non-existing key + if (updated.empty()) return; + + // Adding a new key to an existing section + sectionIt->second.entries.emplace(key, + ValuesData { + .values = std::move(values), + .index = static_cast(sectionIt->second.entries.size()), + }); + changed_ = true; + return; + } + + // Deleting an existing key + if (updated.empty()) { + sectionIt->second.entries.erase(it); + if (sectionIt->second.entries.empty()) data_.sections.erase(sectionIt); + changed_ = true; + return; + } + + // Overriding an existing key + const std::span original = it->second.values.get(); + if (original.size() == updated.size()) { + bool equal = true; + for (size_t i = 0; i < original.size(); ++i) { + if (original[i].value != updated[i].value) { + equal = false; + break; + } + } + if (equal) return; + } + + // Preserve existing comments where not overriden. + for (size_t i = 0, n = std::min(original.size(), updated.size()); i < n; ++i) { + if (!updated[i].comment.has_value() && original[i].comment.has_value()) { + updated[i].comment = std::move(original[i].comment); + } + } + it->second.values = std::move(values); + changed_ = true; +} + +void Ini::set(std::string_view section, std::string_view key, std::span strings) +{ + if (strings.empty()) { + set(section, key, Values {}); + } else if (strings.size() == 1) { + set(section, key, Values { Value { .comment = {}, .value = strings[0] } }); + } else { + Values values; + auto &items = std::get>(values.rep_); + items.reserve(strings.size()); + for (const std::string &str : strings) { + items.push_back(Value { .comment = {}, .value = str }); + } + set(section, key, std::move(values)); + } +} + +void Ini::set(std::string_view section, std::string_view key, std::string &&value) +{ + set(section, key, Values { Value { .comment = {}, .value = std::move(value) } }); +} + +void Ini::set(std::string_view section, std::string_view key, std::string_view value) +{ + + set(section, key, std::string(value)); +} + +void Ini::set(std::string_view section, std::string_view key, int value) +{ + set(section, key, StrCat(value)); +} + +void Ini::set(std::string_view section, std::string_view key, bool value) +{ + set(section, key, std::string(value ? "1" : "0")); +} + +void Ini::set(std::string_view section, std::string_view key, float value) +{ + constexpr size_t BufSize = 64; + char buf[BufSize] {}; + const std::to_chars_result result = std::to_chars(buf, buf + BufSize, value); + if (result.ec != std::errc()) { + app_fatal("float->string failed"); // should never happen + } + set(section, key, std::string_view(buf, result.ptr)); +} + +namespace { + +template +bool OrderByValueIndex(const std::pair &a, const std::pair &b) +{ + return a.second.index < b.second.index; +}; + +// Appends a possibly multi-line comment, converting \r\n to \n. +void AppendComment(std::string_view comment, std::string &out) +{ + bool prevR = false; + for (const char c : comment) { + if (prevR) { + prevR = c != '\r'; + out += c; + } else if (c == '\r') { + prevR = true; + } else { + out += c; + } + } +} + +void AppendSection(std::string_view sectionName, std::string &out) +{ + out.append("[").append(sectionName).append("]\n"); +} + +void AppendKeyValue(std::string_view key, std::string_view value, std::string &out) +{ + out.append(key).append("=").append(value).append("\n"); +} + +} // namespace + +std::string Ini::serialize() const +{ + std::string result; + std::vector> sections(data_.sections.begin(), data_.sections.end()); + c_sort(sections, OrderByValueIndex); + + std::vector> entries; + for (auto &[sectionName, section] : sections) { + if (!result.empty()) result += '\n'; + if (!section.comment.empty()) AppendComment(section.comment, result); + AppendSection(sectionName, result); + entries.assign(section.entries.begin(), section.entries.end()); + c_sort(entries, OrderByValueIndex); + for (const auto &[key, entry] : entries) { + for (const auto &[comment, value] : entry.values.get()) { + if (comment.has_value() && !comment->empty()) AppendComment(*comment, result); + AppendKeyValue(key, value, result); + } + } + } + return result; +} + +} // namespace devilution diff --git a/Source/utils/ini.hpp b/Source/utils/ini.hpp new file mode 100644 index 000000000..dee4cbb97 --- /dev/null +++ b/Source/utils/ini.hpp @@ -0,0 +1,121 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "utils/string_view_hash.hpp" + +namespace devilution { + +class Ini { +public: + // A single value associated with a section and key. + struct Value { + // When setting a value, `nullopt` results + // in preserving the existing comment if any. + std::optional comment; + std::string value; + }; + + // All the values associated with a section and key. + class Values { + public: + /** + * @brief Constructs an empty set of values. + * + * If passed to `set`, the key is deleted. + */ + Values(); + + explicit Values(const Value &data); + + [[nodiscard]] std::span get() const; + [[nodiscard]] std::span get(); + void append(const Value &value); + + private: + // Most keys only have a single value, so we use + // a representation that avoids allocations in that case. + std::variant> rep_; + + friend class Ini; + }; + + static tl::expected parse(std::string_view buffer); + [[nodiscard]] std::string serialize() const; + + /** @return all the values associated with this section and key in the ini */ + [[nodiscard]] std::span get(std::string_view section, std::string_view key) const; + + /** @return the default value if the ini value is unset or empty */ + [[nodiscard]] std::string_view getString(std::string_view section, std::string_view key, std::string_view defaultValue = {}) const; + + /** @return the default value if the ini value is unset or empty */ + [[nodiscard]] bool getBool(std::string_view section, std::string_view key, bool defaultValue) const; + + /** @return the default value if the ini value is unset or empty */ + [[nodiscard]] int getInt(std::string_view section, std::string_view key, int defaultValue) const; + + /** @return the default value if the ini value is unset or empty */ + [[nodiscard]] float getFloat(std::string_view section, std::string_view key, float defaultValue) const; + + void getUtf8Buf(std::string_view section, std::string_view key, std::string_view defaultValue, char *dst, size_t dstSize) const; + + void getUtf8Buf(std::string_view section, std::string_view key, char *dst, size_t dstSize) const + { + getUtf8Buf(section, key, /*defaultValue=*/ {}, dst, dstSize); + } + + [[nodiscard]] bool changed() const { return changed_; } + void markAsUnchanged() { changed_ = false; } + + // If values are empty, deletes the entry. + void set(std::string_view section, std::string_view key, Values &&values); + + void set(std::string_view section, std::string_view key, std::span value); + void set(std::string_view section, std::string_view key, std::string &&value); + void set(std::string_view section, std::string_view key, std::string_view value); + void set(std::string_view section, std::string_view key, const char *value) + { + set(section, key, std::string_view(value)); + } + void set(std::string_view section, std::string_view key, bool value); + void set(std::string_view section, std::string_view key, int value); + void set(std::string_view section, std::string_view key, float value); + +private: + struct ValuesData { + Values values; + uint32_t index; + }; + + struct SectionData { + std::string comment; + ankerl::unordered_dense::map entries; + uint32_t index; + }; + + struct FileData { + ankerl::unordered_dense::map sections; + }; + + explicit Ini(FileData &&data) + : data_(std::move(data)) + { + } + + FileData data_; + bool changed_ = false; +}; + +} // namespace devilution diff --git a/Source/utils/string_or_view.hpp b/Source/utils/string_or_view.hpp index 1d38c75bd..879384741 100644 --- a/Source/utils/string_or_view.hpp +++ b/Source/utils/string_or_view.hpp @@ -14,6 +14,7 @@ public: { } + StringOrView(const StringOrView &) = default; StringOrView(StringOrView &&) noexcept = default; StringOrView(std::string &&str) diff --git a/Source/utils/utf8.cpp b/Source/utils/utf8.cpp index 2c28694ff..50f46975e 100644 --- a/Source/utils/utf8.cpp +++ b/Source/utils/utf8.cpp @@ -41,7 +41,11 @@ std::string_view TruncateUtf8(std::string_view str, std::size_t len) void CopyUtf8(char *dest, std::string_view source, std::size_t bytes) { source = TruncateUtf8(source, bytes - 1); - std::memcpy(dest, source.data(), source.size()); + // source.empty() can mean source.data() == nullptr. + // It is UB to pass a null pointer to memcpy, so we guard against it. + if (!source.empty()) { + std::memcpy(dest, source.data(), source.size()); + } dest[source.size()] = '\0'; } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ce9b73473..a8d4f3e46 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -43,6 +43,7 @@ set(standalone_tests crawl_test file_util_test format_int_test + ini_test parse_int_test str_cat_test utf8_test @@ -93,6 +94,7 @@ target_link_libraries(crawl_benchmark PRIVATE libdevilutionx_crawl) target_link_libraries(dun_render_benchmark PRIVATE libdevilutionx_so) target_link_libraries(file_util_test PRIVATE libdevilutionx_file_util app_fatal_for_testing) target_link_libraries(format_int_test PRIVATE libdevilutionx_format_int language_for_testing) +target_link_libraries(ini_test PRIVATE libdevilutionx_ini app_fatal_for_testing) target_link_libraries(parse_int_test PRIVATE libdevilutionx_parse_int) target_link_libraries(str_cat_test PRIVATE libdevilutionx_strings) target_link_libraries(utf8_test PRIVATE libdevilutionx_utf8) diff --git a/test/ini_test.cpp b/test/ini_test.cpp new file mode 100644 index 000000000..8f489a3c6 --- /dev/null +++ b/test/ini_test.cpp @@ -0,0 +1,86 @@ +#include "utils/ini.hpp" + +#include +#include + +#include + +namespace devilution { +namespace { + +using ::testing::ElementsAre; +using ::testing::Eq; +using ::testing::Field; + +} // namespace + +TEST(IniTest, BasicTest) +{ + tl::expected result = Ini::parse(R"( +; Section A comment +[sectionA] +key1 = value1 +key2 = value2 +; comment multi 1 +multi = a +; comment multi 2 +multi = b +int=-3 +float=2.5 +; bool yes comment +bool yes=1 +bool no=0 + +; Section B comment line 1 +; Section B comment line 2 +[sectionB] +key = value +)"); + + ASSERT_TRUE(result.has_value()) << result.error(); + EXPECT_EQ(result->getString("sectionA", "key1"), "value1"); + EXPECT_EQ(result->getString("sectionA", "key2"), "value2"); + { + const std::span multiVals = result->get("sectionA", "multi"); + std::vector multiStrs; + for (const Ini::Value &val : multiVals) { + multiStrs.push_back(val.value); + } + EXPECT_THAT(multiStrs, ElementsAre(Eq("a"), Eq("b"))); + } + EXPECT_EQ(result->getInt("sectionA", "int", 0), -3); + EXPECT_NEAR(result->getFloat("sectionA", "float", 0.0f), 2.5f, 0.001f); + EXPECT_EQ(result->getString("sectionB", "key"), "value"); + + result->set("newSection", "newKey", "hello"); + result->set("sectionA", "key1", "newValue"); + result->set("sectionA", "int", 1337); + result->set("sectionA", "bool yes", false); + const std::vector newMulti { "x", "y", "z" }; + result->set("sectionA", "multi", newMulti); + result->set("sectionA", "float", 10.5F); + EXPECT_EQ(result->serialize(), std::string_view(R"(; Section A comment +[sectionA] +key1=newValue +key2=value2 +; comment multi 1 +multi=x +; comment multi 2 +multi=y +multi=z +int=1337 +float=10.5 +; bool yes comment +bool yes=0 +bool no=0 + +; Section B comment line 1 +; Section B comment line 2 +[sectionB] +key=value + +[newSection] +newKey=hello +)")); +} +} // namespace devilution diff --git a/tools/make_src_dist.py b/tools/make_src_dist.py index f633814d9..c1db1fb64 100755 --- a/tools/make_src_dist.py +++ b/tools/make_src_dist.py @@ -33,10 +33,10 @@ import subprocess import sys # We only package the dependencies that are: -# 1. Uncommon in package managers (sdl_audiolib and simpleini). +# 1. Uncommon in package managers (sdl_audiolib). # 2. Require devilutionx forks (all others). _DEPS = ['asio', 'libmpq', 'libsmackerdec', - 'libzt', 'sdl_audiolib', 'simpleini', 'unordered_dense'] + 'libzt', 'sdl_audiolib', 'unordered_dense'] _ALWAYS_VENDORED_DEPS = ['asio', 'libmpq', 'libsmackerdec', 'libzt'] # These dependencies are not vendored by default. diff --git a/vcpkg.json b/vcpkg.json index 86982b1c2..f052d7cd5 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -4,7 +4,6 @@ "dependencies": [ "fmt", "bzip2", - "simpleini", "lua" ], "builtin-baseline": "29b2ea2d4b6197e66ef346e62ccbba35b55b7de5",