From 2bcec5524609ec39a376dffe1d56b41e74afcf2a Mon Sep 17 00:00:00 2001 From: so5iso4ka Date: Mon, 10 Aug 2026 15:26:34 +0300 Subject: [PATCH] feat(migration): add an option to migrate from PineconeMC Signed-off-by: so5iso4ka --- launcher/Application.cpp | 67 +++++++--- launcher/CMakeLists.txt | 2 + launcher/ConfigMigration.cpp | 57 ++++++++ launcher/ConfigMigration.h | 93 +++++++++++++ launcher/minecraft/auth/AccountData.cpp | 11 +- tests/CMakeLists.txt | 3 + tests/ConfigMigration_test.cpp | 126 ++++++++++++++++++ .../ConfigMigration/prismlauncher.cfg | 20 +++ .../prismlauncher.filtered.expected.cfg | 11 ++ .../prismlauncher.modified.expected.cfg | 20 +++ 10 files changed, 393 insertions(+), 17 deletions(-) create mode 100644 launcher/ConfigMigration.cpp create mode 100644 launcher/ConfigMigration.h create mode 100644 tests/ConfigMigration_test.cpp create mode 100644 tests/testdata/ConfigMigration/prismlauncher.cfg create mode 100644 tests/testdata/ConfigMigration/prismlauncher.filtered.expected.cfg create mode 100644 tests/testdata/ConfigMigration/prismlauncher.modified.expected.cfg diff --git a/launcher/Application.cpp b/launcher/Application.cpp index c9ebd6968..ac6ce4c79 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -87,6 +87,7 @@ #include "ApplicationMessage.h" +#include #include #include @@ -133,6 +134,8 @@ #include #include + +#include "ConfigMigration.h" #include "SysInfo.h" #ifdef Q_OS_LINUX @@ -265,6 +268,35 @@ void appDebugOutput(QtMsgType type, const QMessageLogContext& context, const QSt fflush(stderr); } +QString combineAppDataPath(const QString& path) +{ + return FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), path); +} + +void handleConfigMigration() +{ + if (!QFile::exists(BuildConfig.LAUNCHER_CONFIGFILE)) { + struct Migration { + QString configFile; + QString pathToExclude; + }; + + if (!ConfigMigration::migrate("elyprismlauncher.cfg", BuildConfig.LAUNCHER_CONFIGFILE, + ConfigMigration::ExcludeByPrefix(combineAppDataPath("../../ElyPrismLauncher")), + ConfigMigration::TransformPineconeMCSettings{})) { + std::array migrations{ Migration{ "prismlauncher.cfg", combineAppDataPath("../../PrismLauncher") }, + Migration{ "polymc.cfg", combineAppDataPath("../../PolyMC") }, + Migration{ "multimc.cfg", combineAppDataPath("../../multimc") } }; + + for (const auto& [conf, path] : migrations) { + if (ConfigMigration::migrate(conf, BuildConfig.LAUNCHER_CONFIGFILE, ConfigMigration::ExcludeByPrefix(path))) { + break; + } + } + } + } +} + } // namespace std::tuple read_lock_File(const QString& path) @@ -586,20 +618,22 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) } { - bool migrated = false; - - if (!migrated) - migrated = handleDataMigration( - dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../PrismLauncher"), - "Prism Launcher", "prismlauncher.cfg"); - if (!migrated) - migrated = handleDataMigration( - dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../PolyMC"), "PolyMC", - "polymc.cfg"); - if (!migrated) - migrated = handleDataMigration( - dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../multimc"), "MultiMC", - "multimc.cfg"); + struct Migration { + QString oldData; + QString name; + QString configFile; + }; + + std::array migrations = { Migration{ combineAppDataPath("../../ElyPrismLauncher"), "PineconeMC", "elyprismlauncher.cfg" }, + Migration{ combineAppDataPath("../../PrismLauncher"), "Prism Launcher", "prismlauncher.cfg" }, + Migration{ combineAppDataPath("../../PolyMC"), "PolyMC", "polymc.cfg" }, + Migration{ combineAppDataPath("../../multimc"), "MultiMC", "multimc.cfg" } }; + + for (const auto& [oldData, name, configFile] : migrations) { + if (handleDataMigration(dataPath, oldData, name, configFile)) { + break; + } + } } { @@ -649,8 +683,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) // Initialize application settings { - // Provide a fallback for migration from PolyMC - m_settings.reset(new INISettingsObject({ BuildConfig.LAUNCHER_CONFIGFILE, "polymc.cfg", "multimc.cfg" }, this)); + // Provide a fallback for migration from some launchers + handleConfigMigration(); + m_settings.reset(new INISettingsObject(BuildConfig.LAUNCHER_CONFIGFILE)); // Date QDate now = QDate::currentDate(); diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index f1fd957f8..02e1529d4 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -872,6 +872,8 @@ SET(LAUNCHER_SOURCES KnownJavaAgents.cpp KnownJavaAgents.h + ConfigMigration.h + ConfigMigration.cpp # GUI - windows ui/GuiUtil.h diff --git a/launcher/ConfigMigration.cpp b/launcher/ConfigMigration.cpp new file mode 100644 index 000000000..7328d1c44 --- /dev/null +++ b/launcher/ConfigMigration.cpp @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Freesm Launcher - Minecraft Launcher + * Copyright (C) 2026 so5iso4ka + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ConfigMigration.h" + +namespace ConfigMigration { +Setting ExcludeByPrefix::operator()(const Setting& s, const QString&) const +{ + if (s.value.toString().startsWith(prefix)) { + return {}; + } + return s; +} + +Setting TransformPineconeMCSettings::operator()(const Setting& setting, const QString& group) const +{ + auto elySkinSettingProj = [](int elyPatchPreference) -> int { + switch (elyPatchPreference) { + case 0: { // Always + return 1; + } + case 1: { // When using Ely and Offline accounts + return 3; + } + case 2: { // When using Ely accounts + return 2; + } + case 3: { // Never + return 0; + } + default: { + return 2; + } + } + }; + + if (group.isEmpty() && setting.key == "ElyPatchPreference" && !setting.value.isNull()) { + return Setting{ "UseElySkins", elySkinSettingProj(setting.value.toInt()) }; + } + return setting; +} +} // namespace ConfigMigration diff --git a/launcher/ConfigMigration.h b/launcher/ConfigMigration.h new file mode 100644 index 000000000..40d8b4a51 --- /dev/null +++ b/launcher/ConfigMigration.h @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Freesm Launcher - Minecraft Launcher + * Copyright (C) 2026 so5iso4ka + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ConfigMigration { +struct Setting { + QString key; + QVariant value; +}; + +template +concept SettingProj = + std::regular_invocable && std::is_convertible_v, Setting>; + +class ExcludeByPrefix { + public: + Setting operator()(const Setting& s, const QString&) const; + + QString prefix; +}; + +class TransformPineconeMCSettings { + public: + Setting operator()(const Setting& setting, const QString& group) const; +}; + +template +bool migrate(const QString& oldConfig, const QString& newConfig, Proj... proj) +{ + if (!QFile::exists(oldConfig)) { + return false; + } + + QSettings oldSettings(oldConfig, QSettings::IniFormat); + QSettings newSettings(newConfig, QSettings::IniFormat); + + std::list groups{ "" }; + { + auto topLevelGroups = oldSettings.childGroups(); + groups.insert(groups.end(), topLevelGroups.begin(), topLevelGroups.end()); + } + + for (auto it = groups.begin(); it != groups.end(); ++it) { + oldSettings.beginGroup(*it); + newSettings.beginGroup(*it); + + if (it != groups.begin()) { + auto innerGroups = oldSettings.childGroups(); + std::ranges::for_each(innerGroups, [currentGroup = *it](QString& group) { group.prepend(currentGroup + '/'); }); + groups.insert(groups.end(), innerGroups.begin(), innerGroups.end()); + } + + auto keys = oldSettings.childKeys(); + for (const auto& key : keys) { + Setting setting{ key, oldSettings.value(key) }; + ((setting = std::invoke(proj, setting, *it)), ...); + if (!setting.key.isEmpty()) { + newSettings.setValue(setting.key, setting.value); + } + } + + newSettings.endGroup(); + oldSettings.endGroup(); + } + return true; +} +} // namespace ConfigMigration diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp index d6ef053c7..6c69b5fb9 100644 --- a/launcher/minecraft/auth/AccountData.cpp +++ b/launcher/minecraft/auth/AccountData.cpp @@ -281,6 +281,7 @@ bool entitlementFromJSONV3(const QJsonObject& parent, MinecraftEntitlement& out) bool AccountData::resumeStateFromV3(QJsonObject data) { + bool migratingFromEPL = false; auto typeV = data.value("type"); if (!typeV.isString()) { qWarning() << "Failed to parse account data: type is missing."; @@ -295,6 +296,9 @@ bool AccountData::resumeStateFromV3(QJsonObject data) type = AccountType::Elyby; } else if (typeS == "Custom") { type = AccountType::Custom; + } else if (typeS == "Ely") { + type = AccountType::Elyby; + migratingFromEPL = true; } else { qWarning() << "Failed to parse account data: type is not recognized."; return false; @@ -313,7 +317,12 @@ bool AccountData::resumeStateFromV3(QJsonObject data) case AccountType::Offline: break; case AccountType::Elyby: { - auto clientIDV = data.value("elyby-client-id"); + QJsonValue clientIDV; + if (migratingFromEPL) { + clientIDV = data.value("msa-client-id"); + } else { + clientIDV = data.value("elyby-client-id"); + } if (clientIDV.isString()) { clientID = clientIDV.toString(); } // leave clientID empty if it doesn't exist or isn't a string diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ee28f0d9e..bdfc11442 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -63,3 +63,6 @@ ecm_add_test(CatPack_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR ecm_add_test(XmlLogs_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME XmlLogs) + +ecm_add_test(ConfigMigration_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME ConfigMigration) diff --git a/tests/ConfigMigration_test.cpp b/tests/ConfigMigration_test.cpp new file mode 100644 index 000000000..2cc9375e4 --- /dev/null +++ b/tests/ConfigMigration_test.cpp @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Freesm Launcher - Minecraft Launcher + * Copyright (C) 2026 so5iso4ka + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include + +#include "ConfigMigration.h" + +namespace { +bool configEqual(const QString& lhsPath, const QString& rhsPath) +{ + QSettings lhs(lhsPath, QSettings::IniFormat), rhs(rhsPath, QSettings::IniFormat); + auto lhsKeys = lhs.allKeys(), rhsKeys = rhs.allKeys(); + + std::ranges::sort(lhsKeys); + std::ranges::sort(rhsKeys); + + if (lhsKeys != rhsKeys) { + return false; + } + + return std::ranges::all_of(lhsKeys, [&lhs, &rhs](const auto& key) { return lhs.value(key) == rhs.value(key); }); +} +} // namespace + +class ConfigMigrationTest : public QObject { + Q_OBJECT + + private slots: + void test_TrivialCopy() + { + class Identity { + public: + ConfigMigration::Setting operator()(const ConfigMigration::Setting& s, const QString&) const { return s; } + }; + + auto dataDir = QDir(QFINDTESTDATA("testdata/ConfigMigration")); + auto from = dataDir.absoluteFilePath("prismlauncher.cfg"); + auto to1 = "prismlauncher.out1.cfg"; + auto to2 = "prismlauncher.out2.cfg"; + auto to3 = "prismlauncher.out3.cfg"; + + QFile::remove(to1); + QFile::remove(to2); + QFile::remove(to3); + + ConfigMigration::migrate(from, to1); + ConfigMigration::migrate(from, to2, Identity{}); + ConfigMigration::migrate(from, to3, Identity{}, Identity{}); + + QVERIFY(configEqual(from, to1)); + QVERIFY(configEqual(from, to2)); + QVERIFY(configEqual(from, to3)); + } + + void test_Modify() + { + auto dataDir = QDir(QFINDTESTDATA("testdata/ConfigMigration")); + auto from = dataDir.absoluteFilePath("prismlauncher.cfg"); + auto to = "prismlauncher.modified.out.cfg"; + + QFile::remove(to); + + ConfigMigration::migrate( + from, to, + [](ConfigMigration::Setting setting, QString group) { + if (group.isEmpty() && setting.key == "CatFit") { + setting.value = "skinny"; + } + if (group.isEmpty() && setting.key == "MaxMemAlloc") { + setting.value = "8192"; + } + return setting; + }, + [](ConfigMigration::Setting setting, QString group) { + if (group.startsWith("Cat") && setting.value.canConvert()) { + setting.value = setting.value.toInt() * 2; + } + return setting; + }); + + QVERIFY(configEqual(to, dataDir.absoluteFilePath("prismlauncher.modified.expected.cfg"))); + } + + void test_Filter() + { + auto dataDir = QDir(QFINDTESTDATA("testdata/ConfigMigration")); + auto from = dataDir.absoluteFilePath("prismlauncher.cfg"); + auto to = "prismlauncher.filtered.out.cfg"; + + QFile::remove(to); + + ConfigMigration::migrate(from, to, [](ConfigMigration::Setting setting, QString group) { + if (group.startsWith("UI") || setting.value.toString().startsWith("/Users/cat/Library/Application Support/PrismLauncher") || + setting.key.endsWith("Geometry")) { + return ConfigMigration::Setting{}; + } + return setting; + }); + + QVERIFY(configEqual(to, dataDir.absoluteFilePath("prismlauncher.filtered.expected.cfg"))); + } +}; + +QTEST_GUILESS_MAIN(ConfigMigrationTest) + +#include "ConfigMigration_test.moc" diff --git a/tests/testdata/ConfigMigration/prismlauncher.cfg b/tests/testdata/ConfigMigration/prismlauncher.cfg new file mode 100644 index 000000000..2fba19f41 --- /dev/null +++ b/tests/testdata/ConfigMigration/prismlauncher.cfg @@ -0,0 +1,20 @@ +[General] +CatFit=fit +ProxyType=Default +ProxyUser= +JavaPath=/Users/cat/Library/Application Support/PrismLauncher/java/java-runtime-delta/bin/java +MinMemAlloc=512 +MaxMemAlloc=4096 +ConsoleWindowGeometry=AdnQywADAAAAAAG0AAAAJgAABRUAAAObAAABtAAAAEIAAAUVAAADmwAAAAAAAAAABegAAAG0AAAAQgAABRUAAAOb + +[UI] +datapacks_Page\ColumnsVisibility="{\"File Name\":false,\"Image\":true,\"Last Modified\":true,\"Pack Format\":true,\"Provider\":true,\"Size\":false,\"Version\":true}" +mods_Page\ColumnsVisibility="{\"File Name\":false,\"Image\":true,\"Last Modified\":true,\"Loaders\":false,\"Minecraft Versions\":false,\"Pack Format\":true,\"Provider\":true,\"Release Type\":false,\"Required By\":false,\"Requires\":false,\"Side\":false,\"Size\":false,\"Version\":true}" +resourcepacks_Page\ColumnsVisibility="{\"File Name\":false,\"Image\":true,\"Last Modified\":true,\"Pack Format\":true,\"Provider\":true,\"Size\":false,\"Version\":true}" +shaderpacks_Page\ColumnsVisibility="{\"File Name\":false,\"Image\":true,\"Last Modified\":true,\"Pack Format\":true,\"Provider\":true,\"Size\":false,\"Version\":true}" +texturepacks_Page\ColumnsVisibility="{\"File Name\":false,\"Image\":true,\"Last Modified\":true,\"Pack Format\":true,\"Provider\":true,\"Size\":false,\"Version\":true}" + +[Cat] +DeepCat=1 +Deep\DeeperCat=2 +Deep\Deeper\DeepestCat=3 \ No newline at end of file diff --git a/tests/testdata/ConfigMigration/prismlauncher.filtered.expected.cfg b/tests/testdata/ConfigMigration/prismlauncher.filtered.expected.cfg new file mode 100644 index 000000000..6a5af86c9 --- /dev/null +++ b/tests/testdata/ConfigMigration/prismlauncher.filtered.expected.cfg @@ -0,0 +1,11 @@ +[General] +CatFit=fit +ProxyType=Default +ProxyUser= +MinMemAlloc=512 +MaxMemAlloc=4096 + +[Cat] +DeepCat=1 +Deep\DeeperCat=2 +Deep\Deeper\DeepestCat=3 \ No newline at end of file diff --git a/tests/testdata/ConfigMigration/prismlauncher.modified.expected.cfg b/tests/testdata/ConfigMigration/prismlauncher.modified.expected.cfg new file mode 100644 index 000000000..3ea26133b --- /dev/null +++ b/tests/testdata/ConfigMigration/prismlauncher.modified.expected.cfg @@ -0,0 +1,20 @@ +[General] +CatFit=skinny +ProxyType=Default +ProxyUser= +JavaPath=/Users/cat/Library/Application Support/PrismLauncher/java/java-runtime-delta/bin/java +MinMemAlloc=512 +MaxMemAlloc=8192 +ConsoleWindowGeometry=AdnQywADAAAAAAG0AAAAJgAABRUAAAObAAABtAAAAEIAAAUVAAADmwAAAAAAAAAABegAAAG0AAAAQgAABRUAAAOb + +[UI] +datapacks_Page\ColumnsVisibility="{\"File Name\":false,\"Image\":true,\"Last Modified\":true,\"Pack Format\":true,\"Provider\":true,\"Size\":false,\"Version\":true}" +mods_Page\ColumnsVisibility="{\"File Name\":false,\"Image\":true,\"Last Modified\":true,\"Loaders\":false,\"Minecraft Versions\":false,\"Pack Format\":true,\"Provider\":true,\"Release Type\":false,\"Required By\":false,\"Requires\":false,\"Side\":false,\"Size\":false,\"Version\":true}" +resourcepacks_Page\ColumnsVisibility="{\"File Name\":false,\"Image\":true,\"Last Modified\":true,\"Pack Format\":true,\"Provider\":true,\"Size\":false,\"Version\":true}" +shaderpacks_Page\ColumnsVisibility="{\"File Name\":false,\"Image\":true,\"Last Modified\":true,\"Pack Format\":true,\"Provider\":true,\"Size\":false,\"Version\":true}" +texturepacks_Page\ColumnsVisibility="{\"File Name\":false,\"Image\":true,\"Last Modified\":true,\"Pack Format\":true,\"Provider\":true,\"Size\":false,\"Version\":true}" + +[Cat] +DeepCat=2 +Deep\DeeperCat=4 +Deep\Deeper\DeepestCat=6 \ No newline at end of file