Browse Source

Migrate from SimpleIni to our own implementation

Our implementation has a more modern interface and only
supports the features that we care about.

It always outputs `\n` as newlines and does not output BOM.

The modern interface eliminates awkward `c_str()/data()` conversions.

This implementation preserves comments and the file order of sections
and keys. New keys are written in insertion order.

We now also support modifying and adding default comments,
which may be a useful thing to do for the especially tricky
ini options (this PR doesn't add any but adds the ability to do so).

Sadly, this increases the RG99 binary size by 24 KiB.
I'm guessing this is because the map implementation generates
quite a bit of code.

Note that while it might seem that using `std::string` for every key and
value would do a lot of allocations, most of these strings are
small and thus benefit from Small String Optimization (= no allocations).
pull/7453/merge
Gleb Mazovetskiy 1 year ago
parent
commit
b8ab6d2faa
  1. 2
      .devcontainer/Dockerfile
  2. 3
      .github/workflows/Linux_aarch64.yml
  3. 4
      .github/workflows/Linux_x86.yml
  4. 3
      .github/workflows/Linux_x86_64.yml
  5. 12
      3rdParty/simpleini/CMakeLists.txt
  6. 16
      CMake/Dependencies.cmake
  7. 2
      Packaging/nix/debian-cross-aarch64-prep.sh
  8. 13
      Source/CMakeLists.txt
  9. 305
      Source/options.cpp
  10. 5
      Source/options.h
  11. 415
      Source/utils/ini.cpp
  12. 121
      Source/utils/ini.hpp
  13. 1
      Source/utils/string_or_view.hpp
  14. 6
      Source/utils/utf8.cpp
  15. 2
      test/CMakeLists.txt
  16. 86
      test/ini_test.cpp
  17. 4
      tools/make_src_dist.py
  18. 1
      vcpkg.json

2
.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/*

3
.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

4
.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

3
.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

12
3rdParty/simpleini/CMakeLists.txt vendored

@ -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)

16
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()

2
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
)

13
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

305
Source/options.cpp

@ -5,14 +5,15 @@
*/
#include <algorithm>
#include <cerrno>
#include <cstdint>
#include <cstdio>
#include <optional>
#include <span>
#include <expected.hpp>
#include <fmt/format.h>
#define SI_NO_CONVERSION
#include <SimpleIni.h>
#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> 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<std::string> GetValue()
{
std::list<CSimpleIni::Entry> values;
if (!GetIni().GetAllValues(sectionName_, keyName_, values))
return std::nullopt;
std::string ret;
for (auto &entry : values) {
if (entry.pItem != nullptr)
ret.append(entry.pItem);
ret.append("\n");
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(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<std::string> oldValue_;
const char *sectionName_;
const char *keyName_;
};
int GetIniInt(const char *keyname, const char *valuename, int defaultValue)
{
return GetIni().GetLongValue(keyname, valuename, defaultValue);
}
bool GetIniBool(const char *sectionName, const char *keyName, bool defaultValue)
{
return GetIni().GetBoolValue(sectionName, keyName, defaultValue);
}
float GetIniFloat(const char *sectionName, const char *keyName, float defaultValue)
{
return (float)GetIni().GetDoubleValue(sectionName, keyName, defaultValue);
}
bool 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<std::string> &stringValues)
{
std::list<CSimpleIni::Entry> values;
if (!GetIni().GetAllValues(sectionName, keyName, values)) {
return false;
}
for (auto &entry : values) {
stringValues.emplace_back(entry.pItem);
}
return true;
}
void SetIniValue(const char *keyname, const char *valuename, int value)
{
IniChangedChecker changedChecker(keyname, valuename);
GetIni().SetLongValue(keyname, valuename, value, nullptr, false, true);
}
void SetIniValue(const char *keyname, const char *valuename, 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<std::string> &stringValues)
{
IniChangedChecker changedChecker(keyname, valuename);
bool firstSet = true;
for (auto &value : stringValues) {
GetIni().SetValue(keyname, valuename, value.c_str(), nullptr, firstSet);
firstSet = false;
std::fclose(file);
}
if (firstSet)
GetIni().SetValue(keyname, valuename, "", nullptr, true);
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 (!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<const Ini::Value> values = ini->get("NetMsg", QuickMessages[i].key);
std::vector<std::string> &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> 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<char, 64> result;
if (!GetIniValue(category.data(), key.data(), result.data(), result.size())) {
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.
}
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<char, 64> result;
if (!GetIniValue(category.data(), key.data(), result.data(), result.size())) {
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(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<size_t>(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

5
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<Action> actions;
ankerl::unordered_dense::segmented_map<uint32_t, std::reference_wrapper<Action>> keyIDToAction;
ankerl::unordered_dense::segmented_map<uint32_t, std::string> keyIDToKeyName;
ankerl::unordered_dense::segmented_map<std::string, uint32_t> keyNameToKeyID;
ankerl::unordered_dense::segmented_map<std::string, uint32_t, StringViewHash, StringViewEquals> keyNameToKeyID;
};
/** The Padmapper maps gamepad buttons to actions. */
@ -782,7 +783,7 @@ private:
std::forward_list<Action> actions;
std::array<const Action *, enum_size<ControllerButton>::value> buttonToReleaseAction;
std::array<std::string, enum_size<ControllerButton>::value> buttonToButtonName;
ankerl::unordered_dense::segmented_map<std::string, ControllerButton> buttonNameToButton;
ankerl::unordered_dense::segmented_map<std::string, ControllerButton, StringViewHash, StringViewEquals> buttonNameToButton;
bool committed = false;
const Action *FindAction(ControllerButton button) const;

415
Source/utils/ini.cpp

@ -0,0 +1,415 @@
#include "utils/ini.hpp"
#include <algorithm>
#include <charconv>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <span>
#include <string>
#include <string_view>
#include <system_error>
#include <utility>
#include <variant>
#include <vector>
#include <ankerl/unordered_dense.h>
#include <expected.hpp>
#include <fmt/core.h>
#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<Value> {})
{
}
Ini::Values::Values(const Value &data)
: rep_(data)
{
}
std::span<const Ini::Value> Ini::Values::get() const
{
if (std::holds_alternative<Ini::Value>(rep_)) {
return { &std::get<Ini::Value>(rep_), 1 };
}
return std::get<std::vector<Ini::Value>>(rep_);
}
std::span<Ini::Value> Ini::Values::get()
{
if (std::holds_alternative<Ini::Value>(rep_)) {
return { &std::get<Ini::Value>(rep_), 1 };
}
return std::get<std::vector<Ini::Value>>(rep_);
}
void Ini::Values::append(const Ini::Value &value)
{
if (std::holds_alternative<Value>(rep_)) {
rep_ = std::vector<Value> { std::get<Value>(rep_), value };
return;
}
std::get<std::vector<Value>>(rep_).push_back(value);
}
tl::expected<Ini, std::string> Ini::parse(std::string_view buffer)
{
Ini::FileData fileData;
ankerl::unordered_dense::map<std::string, ValuesData, StringViewHash, StringViewEquals> *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<const char *>(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<uint32_t>(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<const char *>(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<uint32_t>(sectionEntries->size()),
});
}
commentBegin = nullptr;
}
return Ini(std::move(fileData));
}
std::span<const Ini::Value> 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<const Ini::Value> 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<const Ini::Value> 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<const Ini::Value> 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<const Ini::Value> 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<Value> 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<uint32_t>(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<uint32_t>(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<Value> 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<const std::string> 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<std::vector<Value>>(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 <typename T>
bool OrderByValueIndex(const std::pair<std::string, T> &a, const std::pair<std::string, T> &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<std::pair<std::string, SectionData>> sections(data_.sections.begin(), data_.sections.end());
c_sort(sections, OrderByValueIndex<SectionData>);
std::vector<std::pair<std::string, ValuesData>> 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<ValuesData>);
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

121
Source/utils/ini.hpp

@ -0,0 +1,121 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <utility>
#include <variant>
#include <vector>
#include <ankerl/unordered_dense.h>
#include <expected.hpp>
#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<std::string> 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<const Value> get() const;
[[nodiscard]] std::span<Value> 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<Value, std::vector<Value>> rep_;
friend class Ini;
};
static tl::expected<Ini, std::string> 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<const Value> 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<const std::string> 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<std::string, ValuesData, StringViewHash, StringViewEquals> entries;
uint32_t index;
};
struct FileData {
ankerl::unordered_dense::map<std::string, SectionData, StringViewHash, StringViewEquals> sections;
};
explicit Ini(FileData &&data)
: data_(std::move(data))
{
}
FileData data_;
bool changed_ = false;
};
} // namespace devilution

1
Source/utils/string_or_view.hpp

@ -14,6 +14,7 @@ public:
{
}
StringOrView(const StringOrView &) = default;
StringOrView(StringOrView &&) noexcept = default;
StringOrView(std::string &&str)

6
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';
}

2
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)

86
test/ini_test.cpp

@ -0,0 +1,86 @@
#include "utils/ini.hpp"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <string_view>
namespace devilution {
namespace {
using ::testing::ElementsAre;
using ::testing::Eq;
using ::testing::Field;
} // namespace
TEST(IniTest, BasicTest)
{
tl::expected<Ini, std::string> 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<const Ini::Value> multiVals = result->get("sectionA", "multi");
std::vector<std::string> 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<std::string> 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

4
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.

1
vcpkg.json

@ -4,7 +4,6 @@
"dependencies": [
"fmt",
"bzip2",
"simpleini",
"lua"
],
"builtin-baseline": "29b2ea2d4b6197e66ef346e62ccbba35b55b7de5",

Loading…
Cancel
Save