Skip to content

Commit 335f6b6

Browse files
committed
Settings: Ensure INI gets saved in logical section order
1 parent 06708e0 commit 335f6b6

File tree

9 files changed

+359
-42
lines changed

9 files changed

+359
-42
lines changed

src/core/core.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ bool Core::InitializeBaseSettingsLayer(std::string settings_path, Error* error)
251251
else
252252
{
253253
SetDefaultSettings(si, true, true, true);
254-
if (!si.Save(error))
254+
if (!si.Save(error, Settings::GetSectionSaveOrder()))
255255
{
256256
Error::AddPrefix(error, "Failed to save settings: ");
257257
return false;
@@ -277,7 +277,7 @@ bool Core::InitializeBaseSettingsLayer(std::string settings_path, Error* error)
277277
bool Core::SaveBaseSettingsLayer(Error* error)
278278
{
279279
INISettingsInterface& si = s_locals.base_settings_interface;
280-
if (si.IsDirty() && !si.Save(error))
280+
if (si.IsDirty() && !si.Save(error, Settings::GetSectionSaveOrder()))
281281
return false;
282282

283283
return true;

src/core/fullscreenui_settings.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3159,7 +3159,7 @@ void FullscreenUI::DoSaveInputProfile(const std::string& name)
31593159
const auto lock = Core::GetSettingsLock();
31603160
SettingsInterface* ssi = GetEditingSettingsInterface();
31613161
InputManager::CopyConfiguration(&dsi, *ssi, true, false, true, false);
3162-
if (dsi.Save())
3162+
if (dsi.Save(nullptr, Settings::GetSectionSaveOrder()))
31633163
ShowToast(OSDMessageType::Quick, {}, fmt::format(FSUI_FSTR("Controller preset '{}' saved."), name));
31643164
else
31653165
ShowToast(OSDMessageType::Info, {}, fmt::format(FSUI_FSTR("Failed to save controller preset '{}'."), name));

src/core/settings.cpp

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,59 @@ const MediaCaptureBackend Settings::DEFAULT_MEDIA_CAPTURE_BACKEND = MediaCapture
155155
const MediaCaptureBackend Settings::DEFAULT_MEDIA_CAPTURE_BACKEND = MediaCaptureBackend::MaxCount;
156156
#endif
157157

158+
std::span<const char* const> Settings::GetSectionSaveOrder()
159+
{
160+
static constexpr std::array order = {
161+
// clang-format off
162+
"Patches",
163+
"Cheats",
164+
"Main",
165+
"UI",
166+
"GameListTableView",
167+
"AutoUpdater",
168+
"Folders",
169+
"GameList",
170+
"Cheevos",
171+
"Logging",
172+
"BIOS",
173+
"Console",
174+
"CPU",
175+
"GPU",
176+
"Display",
177+
"CDROM",
178+
"Audio",
179+
"MemoryCards",
180+
"TextureReplacements",
181+
"MediaCapture",
182+
"InternalPostProcessing",
183+
"PostProcessing",
184+
"BorderOverlay",
185+
"InputSources",
186+
#ifdef ENABLE_SDL
187+
"SDLExtra",
188+
#endif
189+
"ControllerPorts",
190+
"Pad1",
191+
"Pad2",
192+
"Pad3",
193+
"Pad4",
194+
"Pad5",
195+
"Pad6",
196+
"Pad7",
197+
"Pad8",
198+
"Hotkeys",
199+
"PIO",
200+
"SIO",
201+
"PCDrv",
202+
"Debug",
203+
"DebugWindows",
204+
"Hacks",
205+
// clang-format on
206+
};
207+
208+
return order;
209+
}
210+
158211
Settings::Settings()
159212
{
160213
display_osd_margin = ImGuiManager::DEFAULT_SCREEN_MARGIN;

src/core/settings.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,8 @@ struct Settings : public GPUSettings
471471

472472
bool AreGPUDeviceSettingsChanged(const Settings& old_settings) const;
473473

474+
static std::span<const char* const> GetSectionSaveOrder();
475+
474476
/// Initializes configuration.
475477
static void SetDefaultLogConfig(SettingsInterface& si);
476478
static void UpdateLogConfig(const SettingsInterface& si);

src/duckstation-qt/controllersettingswindow.cpp

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include "util/input_manager.h"
1616

1717
#include "common/assert.h"
18+
#include "common/error.h"
1819
#include "common/file_system.h"
1920

2021
#include <QtWidgets/QInputDialog>
@@ -227,11 +228,13 @@ void ControllerSettingsWindow::onNewProfileClicked()
227228
}
228229
}
229230

230-
if (!temp_si.Save())
231+
Error error;
232+
if (!temp_si.Save(&error, Settings::GetSectionSaveOrder()))
231233
{
232-
QtUtils::AsyncMessageBox(
233-
this, QMessageBox::Critical, tr("Error"),
234-
tr("Failed to save the new preset to '%1'.").arg(QString::fromStdString(temp_si.GetPath())));
234+
QtUtils::AsyncMessageBox(this, QMessageBox::Critical, tr("Error"),
235+
tr("Failed to save the new preset to '%1':\n%2")
236+
.arg(QString::fromStdString(temp_si.GetPath()))
237+
.arg(QString::fromStdString(error.GetDescription())));
235238
return;
236239
}
237240

@@ -316,7 +319,14 @@ void ControllerSettingsWindow::onCopyGlobalSettingsClicked()
316319
false);
317320
}
318321

319-
m_editing_settings_interface->Save();
322+
Error error;
323+
if (!m_editing_settings_interface->Save(&error, Settings::GetSectionSaveOrder()))
324+
{
325+
QtUtils::AsyncMessageBox(this, QMessageBox::Critical, tr("Error"),
326+
tr("Failed to save the preset:\n%1").arg(QString::fromStdString(error.GetDescription())));
327+
return;
328+
}
329+
320330
g_core_thread->reloadGameSettings();
321331
createWidgets();
322332

@@ -409,7 +419,7 @@ void ControllerSettingsWindow::clearSettingValue(const char* section, const char
409419
if (m_editing_settings_interface)
410420
{
411421
m_editing_settings_interface->DeleteValue(section, key);
412-
m_editing_settings_interface->Save();
422+
m_editing_settings_interface->Save(nullptr, Settings::GetSectionSaveOrder());
413423
g_core_thread->reloadGameSettings();
414424
}
415425
else

src/duckstation-qt/qthost.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -697,7 +697,7 @@ bool QtHost::SaveGameSettings(SettingsInterface* sif, bool delete_if_empty)
697697
// see above
698698
const auto lock = Core::GetSettingsLock();
699699

700-
if (!ini->Save(&error))
700+
if (!ini->Save(&error, Settings::GetSectionSaveOrder()))
701701
{
702702
Host::ReportErrorAsync(
703703
TRANSLATE_SV("QtHost", "Error"),

src/util-tests/ini_settings_interface_tests.cpp

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -846,3 +846,198 @@ TEST(INISettingsInterface, SetPathDirtyFlag)
846846
si.SetPath("/tmp/new_path.ini");
847847
EXPECT_TRUE(si.IsDirty());
848848
}
849+
850+
// ---- Ordered Save ----
851+
852+
TEST(INISettingsInterface, OrderedSaveEmptyOrder)
853+
{
854+
INISettingsInterface si;
855+
si.LoadFromString("[B]\nb = 2\n\n[A]\na = 1\n");
856+
857+
const std::string without_order = si.SaveToString();
858+
const std::string with_empty_order = si.SaveToString({});
859+
EXPECT_EQ(without_order, with_empty_order);
860+
}
861+
862+
TEST(INISettingsInterface, OrderedSaveBasic)
863+
{
864+
INISettingsInterface si;
865+
si.LoadFromString("[Alpha]\na = 1\n\n[Beta]\nb = 2\n\n[Gamma]\ng = 3\n");
866+
867+
// Request Gamma first, then Alpha.
868+
const char* const order[] = {"Gamma", "Alpha"};
869+
const std::string output = si.SaveToString(order);
870+
EXPECT_EQ(output,
871+
"[Gamma]\ng = 3\n\n"
872+
"[Alpha]\na = 1\n\n"
873+
"[Beta]\nb = 2\n");
874+
}
875+
876+
TEST(INISettingsInterface, OrderedSaveAllSections)
877+
{
878+
INISettingsInterface si;
879+
si.LoadFromString("[A]\na = 1\n\n[B]\nb = 2\n\n[C]\nc = 3\n");
880+
881+
// Reverse the natural alphabetical order.
882+
const char* const order[] = {"C", "B", "A"};
883+
const std::string output = si.SaveToString(order);
884+
EXPECT_EQ(output,
885+
"[C]\nc = 3\n\n"
886+
"[B]\nb = 2\n\n"
887+
"[A]\na = 1\n");
888+
}
889+
890+
TEST(INISettingsInterface, OrderedSaveNonExistentSections)
891+
{
892+
INISettingsInterface si;
893+
si.LoadFromString("[A]\na = 1\n\n[B]\nb = 2\n");
894+
895+
// "Missing" doesn't exist; should be silently skipped.
896+
const char* const order[] = {"Missing", "B"};
897+
const std::string output = si.SaveToString(order);
898+
EXPECT_EQ(output,
899+
"[B]\nb = 2\n\n"
900+
"[A]\na = 1\n");
901+
}
902+
903+
TEST(INISettingsInterface, OrderedSaveRemainingInAlphabeticalOrder)
904+
{
905+
INISettingsInterface si;
906+
si.LoadFromString("[Delta]\nd = 4\n\n[Alpha]\na = 1\n\n[Charlie]\nc = 3\n\n[Bravo]\nb = 2\n");
907+
908+
// Only specify Charlie; rest should come after in alphabetical order.
909+
const char* const order[] = {"Charlie"};
910+
const std::string output = si.SaveToString(order);
911+
EXPECT_EQ(output,
912+
"[Charlie]\nc = 3\n\n"
913+
"[Alpha]\na = 1\n\n"
914+
"[Bravo]\nb = 2\n\n"
915+
"[Delta]\nd = 4\n");
916+
}
917+
918+
TEST(INISettingsInterface, OrderedSaveSingleSection)
919+
{
920+
INISettingsInterface si;
921+
si.LoadFromString("[Only]\nkey = val\n");
922+
923+
const char* const order[] = {"Only"};
924+
const std::string output = si.SaveToString(order);
925+
EXPECT_EQ(output, "[Only]\nkey = val\n");
926+
}
927+
928+
TEST(INISettingsInterface, OrderedSaveEmptyINI)
929+
{
930+
INISettingsInterface si;
931+
si.LoadFromString("");
932+
933+
const char* const order[] = {"A", "B"};
934+
const std::string output = si.SaveToString(order);
935+
EXPECT_TRUE(output.empty());
936+
}
937+
938+
TEST(INISettingsInterface, OrderedSaveDuplicateOrderEntries)
939+
{
940+
INISettingsInterface si;
941+
si.LoadFromString("[A]\na = 1\n\n[B]\nb = 2\n");
942+
943+
// "A" appears twice; section A should only be written once.
944+
const char* const order[] = {"A", "B", "A"};
945+
const std::string output = si.SaveToString(order);
946+
EXPECT_EQ(output,
947+
"[A]\na = 1\n\n"
948+
"[B]\nb = 2\n");
949+
}
950+
951+
TEST(INISettingsInterface, OrderedSaveContentPreserved)
952+
{
953+
INISettingsInterface si;
954+
si.LoadFromString("[Z]\n"
955+
"plain = hello\n"
956+
"quoted = \"value ; with # chars\"\n\n"
957+
"[A]\n"
958+
"num = 42\n");
959+
960+
const char* const order[] = {"Z"};
961+
const std::string output = si.SaveToString(order);
962+
963+
// Z comes first (as ordered), A follows. Quoting is preserved on save.
964+
EXPECT_EQ(output,
965+
"[Z]\nplain = hello\nquoted = \"value ; with # chars\"\n\n"
966+
"[A]\nnum = 42\n");
967+
}
968+
969+
TEST(INISettingsInterface, OrderedSavePrefixMatching)
970+
{
971+
INISettingsInterface si;
972+
si.LoadFromString("[Other]\no = 0\n\n"
973+
"[Pad]\np = 1\n\n"
974+
"[Pad/1]\np1 = 2\n\n"
975+
"[Pad/2]\np2 = 3\n");
976+
977+
// "Pad" should match "Pad" (exact) and "Pad/1", "Pad/2" (prefix with /).
978+
const char* const order[] = {"Pad"};
979+
const std::string output = si.SaveToString(order);
980+
EXPECT_EQ(output,
981+
"[Pad]\np = 1\n\n"
982+
"[Pad/1]\np1 = 2\n\n"
983+
"[Pad/2]\np2 = 3\n\n"
984+
"[Other]\no = 0\n");
985+
}
986+
987+
TEST(INISettingsInterface, OrderedSavePrefixBoundary)
988+
{
989+
INISettingsInterface si;
990+
si.LoadFromString("[Pad]\np = 1\n\n"
991+
"[Pad/1]\np1 = 2\n\n"
992+
"[Pad2]\np2 = 3\n\n"
993+
"[Padding]\npd = 4\n");
994+
995+
// "Pad" should match "Pad" and "Pad/1", but NOT "Pad2" or "Padding".
996+
const char* const order[] = {"Pad"};
997+
const std::string output = si.SaveToString(order);
998+
EXPECT_EQ(output,
999+
"[Pad]\np = 1\n\n"
1000+
"[Pad/1]\np1 = 2\n\n"
1001+
"[Pad2]\np2 = 3\n\n"
1002+
"[Padding]\npd = 4\n");
1003+
}
1004+
1005+
TEST(INISettingsInterface, OrderedSavePrefixGroupsPreserveOrder)
1006+
{
1007+
INISettingsInterface si;
1008+
si.LoadFromString("[Pad/3]\np3 = 3\n\n"
1009+
"[Pad/1]\np1 = 1\n\n"
1010+
"[Pad/2]\np2 = 2\n\n"
1011+
"[Other]\no = 0\n");
1012+
1013+
// Sub-sections should appear in their natural (alphabetical) order within the prefix group.
1014+
const char* const order[] = {"Pad"};
1015+
const std::string output = si.SaveToString(order);
1016+
EXPECT_EQ(output,
1017+
"[Pad/1]\np1 = 1\n\n"
1018+
"[Pad/2]\np2 = 2\n\n"
1019+
"[Pad/3]\np3 = 3\n\n"
1020+
"[Other]\no = 0\n");
1021+
}
1022+
1023+
TEST(INISettingsInterface, OrderedSaveMultiplePrefixes)
1024+
{
1025+
INISettingsInterface si;
1026+
si.LoadFromString("[Hotkey]\nh = 0\n\n"
1027+
"[Hotkey/1]\nh1 = 1\n\n"
1028+
"[Other]\no = 0\n\n"
1029+
"[Pad]\np = 0\n\n"
1030+
"[Pad/1]\np1 = 1\n\n"
1031+
"[Pad/2]\np2 = 2\n");
1032+
1033+
// Pad group first, then Hotkey group, then remaining.
1034+
const char* const order[] = {"Pad", "Hotkey"};
1035+
const std::string output = si.SaveToString(order);
1036+
EXPECT_EQ(output,
1037+
"[Pad]\np = 0\n\n"
1038+
"[Pad/1]\np1 = 1\n\n"
1039+
"[Pad/2]\np2 = 2\n\n"
1040+
"[Hotkey]\nh = 0\n\n"
1041+
"[Hotkey/1]\nh1 = 1\n\n"
1042+
"[Other]\no = 0\n");
1043+
}

0 commit comments

Comments
 (0)