Files
minecraft-launcher/launcherbackend.cpp
T

750 lines
28 KiB
C++
Raw Normal View History

#include "launcherbackend.h"
2026-08-22 18:19:13 +03:00
#include "authservice.h"
#include "gamelauncher.h"
#include "javalocator.h"
2026-08-23 03:30:31 +03:00
#include "launcherpaths.h"
2026-08-22 18:19:13 +03:00
2026-06-19 21:03:37 +03:00
#include <QDesktopServices>
#include <QDir>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
2026-08-22 18:19:13 +03:00
#include <QRegularExpression>
2026-08-23 03:30:31 +03:00
#include <QTimer>
2026-06-19 21:03:37 +03:00
#include <QUrl>
#include <algorithm>
2026-08-22 18:19:13 +03:00
namespace {
const QString kOffline = QStringLiteral("offline");
const QString kElyBy = QStringLiteral("elyby");
int settingInt(const QVariantMap &settings, const QString &key, int fallback)
{
const QVariant value = settings.value(key);
bool ok = false;
const int parsed = value.toInt(&ok);
return ok ? parsed : fallback;
}
} // namespace
LauncherBackend::LauncherBackend(QObject *parent)
: QObject(parent)
2026-08-22 18:19:13 +03:00
, m_auth(new AuthService(this))
, m_launcher(new GameLauncher(this))
{
2026-08-23 03:30:31 +03:00
// Папка лаунчера создаётся при первом запуске и проверяется при каждом
// следующем — если её удалили, она появится снова.
QString storageError;
if (!LauncherPaths::ensureRootExists(&storageError))
m_storageIssues << tr("Не удалось создать папку лаунчера: %1").arg(storageError);
2026-08-22 18:19:13 +03:00
loadSettings();
loadData();
2026-08-23 03:30:31 +03:00
ensureDataFiles();
2026-08-22 18:19:13 +03:00
connect(m_auth, &AuthService::progress, this, &LauncherBackend::launchProgress);
connect(m_launcher, &GameLauncher::progress, this, &LauncherBackend::launchProgress);
connect(m_launcher, &GameLauncher::output, this, &LauncherBackend::gameOutput);
connect(m_launcher, &GameLauncher::gameStarted, this, [this](const QString &commandLine) {
emit gameOutput(commandLine);
emit gameRunningChanged();
});
connect(m_launcher, &GameLauncher::gameFinished, this, [this](int exitCode, bool crashed) {
emit gameRunningChanged();
emit gameFinished(exitCode, crashed);
});
2026-08-23 03:30:31 +03:00
// Интерфейс подключается к сигналам уже после конструктора — сообщаем отложенно.
if (!m_storageIssues.isEmpty()) {
QTimer::singleShot(0, this, [this] {
emit launchError(m_storageIssues.join(u'\n'));
});
}
}
2026-08-22 18:19:13 +03:00
LauncherBackend::~LauncherBackend() = default;
QStringList LauncherBackend::profileNames() const
{
QStringList names;
for (const auto &p : m_profiles)
names << p.name;
return names;
}
QStringList LauncherBackend::versionNames() const
{
QStringList names;
for (const auto &v : m_versions)
names << v.name;
return names;
}
2026-08-22 18:19:13 +03:00
QStringList LauncherBackend::installedVersions() const
{
return VersionLoader::installedVersions(gameDir());
}
bool LauncherBackend::gameRunning() const
{
2026-08-22 18:19:13 +03:00
return m_launcher->isRunning();
}
void LauncherBackend::addProfile(const QString &name,
const QString &login,
const QString &password,
const QString &authType)
{
Profile profile;
profile.id = m_nextProfileId++;
profile.name = name;
profile.login = login;
profile.password = password;
profile.authType = (authType == kElyBy) ? kElyBy : kOffline;
profile.clientToken = AuthService::generateClientToken();
m_profiles.append(profile);
saveProfiles();
emit profilesChanged();
}
2026-08-22 23:39:46 +00:00
void LauncherBackend::addVersion(const QString &name, const QString &serverUrl, const QString &versionId)
{
2026-08-22 18:19:13 +03:00
m_versions.append({m_nextVersionId++, name, serverUrl, versionId});
saveVersions();
emit versionsChanged();
}
2026-08-22 18:19:13 +03:00
void LauncherBackend::updateProfile(int index,
const QString &name,
const QString &login,
const QString &password,
const QString &authType)
2026-06-16 12:16:59 +03:00
{
if (index < 0 || index >= m_profiles.size())
return;
2026-08-22 18:19:13 +03:00
Profile &profile = m_profiles[index];
// Смена логина или пароля обесценивает сохранённый токен Ely.by.
if (profile.login != login || profile.password != password) {
profile.accessToken.clear();
profile.uuid.clear();
profile.resolvedName.clear();
}
profile.name = name;
profile.login = login;
profile.password = password;
profile.authType = (authType == kElyBy) ? kElyBy : kOffline;
if (profile.clientToken.isEmpty())
profile.clientToken = AuthService::generateClientToken();
2026-06-16 12:16:59 +03:00
saveProfiles();
emit profilesChanged();
}
2026-08-22 18:19:13 +03:00
void LauncherBackend::updateVersion(int index,
const QString &name,
const QString &serverUrl,
const QString &versionId)
2026-06-16 12:16:59 +03:00
{
if (index < 0 || index >= m_versions.size())
return;
m_versions[index].name = name;
m_versions[index].serverUrl = serverUrl;
2026-08-22 18:19:13 +03:00
m_versions[index].versionId = versionId;
2026-06-16 12:16:59 +03:00
saveVersions();
emit versionsChanged();
}
QVariantMap LauncherBackend::profileAt(int index) const
{
if (index < 0 || index >= m_profiles.size())
return {};
const auto &p = m_profiles[index];
2026-08-22 18:19:13 +03:00
return {{"name", p.name},
{"login", p.login},
{"password", p.password},
{"authType", p.authType}};
2026-06-16 12:16:59 +03:00
}
QVariantMap LauncherBackend::versionAt(int index) const
{
if (index < 0 || index >= m_versions.size())
return {};
const auto &v = m_versions[index];
2026-08-22 18:19:13 +03:00
return {{"name", v.name}, {"serverUrl", v.serverUrl}, {"versionId", versionIdOf(v)}};
2026-06-16 12:16:59 +03:00
}
void LauncherBackend::removeProfile(int index)
{
if (index < 0 || index >= m_profiles.size())
return;
m_profiles.removeAt(index);
saveProfiles();
emit profilesChanged();
}
void LauncherBackend::removeVersion(int index)
{
if (index < 0 || index >= m_versions.size())
return;
m_versions.removeAt(index);
saveVersions();
emit versionsChanged();
}
2026-08-22 18:19:13 +03:00
QString LauncherBackend::versionIdOf(const Version &version) const
{
// Пользователь мог назвать запись как угодно — тогда версией считаем само имя.
return version.versionId.isEmpty() ? version.name : version.versionId;
}
QStringList LauncherBackend::checkInstallation(int versionIndex) const
{
if (versionIndex < 0 || versionIndex >= m_versions.size())
return {tr("Версия не выбрана")};
2026-08-23 03:30:31 +03:00
const Version &entry = m_versions[versionIndex];
const QString id = versionIdOf(entry);
if (id.trimmed().isEmpty())
return {tr("У записи лаунчера не указан ID версии. Проверьте %1")
.arg(QDir::toNativeSeparators(LauncherPaths::versionsFile()))};
2026-08-22 18:19:13 +03:00
QString error;
const MinecraftVersion version = VersionLoader::load(gameDir(), id, {}, &error);
2026-08-23 03:30:31 +03:00
if (!version.isValid()) {
return {error.isEmpty()
? tr("Версия «%1» из записи «%2» не установлена в %3")
.arg(id,
entry.name,
QDir::toNativeSeparators(gameDir() + QStringLiteral("/versions")))
: error};
}
2026-08-22 18:19:13 +03:00
LaunchOptions options;
options.gameDir = gameDir();
options.versionId = id;
return GameLauncher::missingFiles(options, version);
}
void LauncherBackend::launchGame(int profileIndex, int versionIndex)
{
2026-08-22 18:19:13 +03:00
if (m_busy) {
emit launchError(tr("Запуск уже выполняется"));
return;
}
if (m_launcher->isRunning()) {
emit launchError(tr("Игра уже запущена"));
return;
}
if (profileIndex < 0 || profileIndex >= m_profiles.size()) {
2026-08-22 18:19:13 +03:00
emit launchError(tr("Выберите профиль перед запуском"));
return;
}
if (versionIndex < 0 || versionIndex >= m_versions.size()) {
2026-08-22 18:19:13 +03:00
emit launchError(tr("Выберите версию перед запуском"));
return;
}
2026-08-23 03:30:31 +03:00
// Дальше проверяется только папка игры: свои файлы лаунчер уже разложил
// в galeonLauncher при запуске.
2026-08-22 18:19:13 +03:00
const QString directory = gameDir();
if (!QDir(directory).exists()) {
2026-08-23 03:30:31 +03:00
emit launchError(tr("Папка .minecraft не найдена: %1. Укажите путь к ней в настройках.")
.arg(QDir::toNativeSeparators(directory)));
return;
}
const Version &entry = m_versions[versionIndex];
const QString id = versionIdOf(entry);
if (id.trimmed().isEmpty()) {
emit launchError(tr("У записи лаунчера не указан ID версии. Проверьте %1")
.arg(QDir::toNativeSeparators(LauncherPaths::versionsFile())));
2026-08-22 18:19:13 +03:00
return;
}
2026-08-23 03:30:31 +03:00
const QString versionsDir = QDir::toNativeSeparators(directory + QStringLiteral("/versions"));
2026-08-22 18:19:13 +03:00
const QStringList installed = VersionLoader::installedVersions(directory);
if (!installed.contains(id)) {
2026-08-22 23:39:46 +00:00
emit launchError(installed.isEmpty()
2026-08-23 03:30:31 +03:00
? tr("В %1 нет ни одной установленной версии").arg(versionsDir)
: tr("Версия «%1» из записи «%2» не установлена в %3. Доступны: %4")
.arg(id,
entry.name,
versionsDir,
installed.join(QStringLiteral(", "))));
2026-08-22 18:19:13 +03:00
return;
}
setBusy(true);
emit launchProgress(tr("Чтение описания версии «%1»…").arg(id));
QSet<QString> features;
if (settingInt(m_settings, QStringLiteral("windowWidth"), 0) > 0
&& settingInt(m_settings, QStringLiteral("windowHeight"), 0) > 0)
features << QStringLiteral("has_custom_resolution");
QString error;
m_pendingVersionData = VersionLoader::load(directory, id, features, &error);
if (!m_pendingVersionData.isValid()) {
failLaunch(error.isEmpty() ? tr("Не удалось прочитать версию «%1»").arg(id) : error);
return;
}
LaunchOptions probe;
probe.gameDir = directory;
probe.versionId = id;
const QStringList missing = GameLauncher::missingFiles(probe, m_pendingVersionData);
if (!missing.isEmpty()) {
failLaunch(tr("В папке игры не хватает файлов (%1):\n%2")
.arg(missing.size())
.arg(missing.join(u'\n')));
return;
}
m_pendingProfile = profileIndex;
m_pendingVersion = versionIndex;
m_pendingInjectorPath.clear();
beginAuthentication();
}
void LauncherBackend::beginAuthentication()
{
const Profile &profile = m_profiles[m_pendingProfile];
if (profile.authType != kElyBy) {
const QString nickname = profile.login.isEmpty() ? profile.name : profile.login;
const AuthResult result = AuthService::offline(nickname);
if (!result.ok) {
failLaunch(result.error);
return;
}
continueLaunch(result);
return;
}
// Ely.by работает только вместе с authlib-injector — без него игра пойдёт
// в сессионный сервер Mojang и токен окажется недействительным.
2026-08-23 03:30:31 +03:00
m_auth->ensureAuthlibInjector(LauncherPaths::runtimeDir(),
[this](const QString &path, const QString &error) {
2026-08-22 18:19:13 +03:00
if (m_pendingProfile < 0)
2026-08-22 23:39:46 +00:00
return; // запуск отменили, пока качался инжектор
2026-08-22 18:19:13 +03:00
if (!error.isEmpty()) {
failLaunch(error);
return;
}
m_pendingInjectorPath = path;
const Profile &pending = m_profiles[m_pendingProfile];
m_auth->loginElyBy(pending.login,
pending.password,
pending.clientToken,
pending.accessToken,
[this](const AuthResult &result) {
if (m_pendingProfile < 0)
return;
if (result.twoFactorRequired) {
setBusy(false);
emit twoFactorRequired(m_profiles[m_pendingProfile].name);
return;
}
if (!result.ok) {
failLaunch(result.error);
return;
}
continueLaunch(result);
});
});
}
void LauncherBackend::submitTwoFactorCode(const QString &code)
{
if (m_pendingProfile < 0 || m_pendingProfile >= m_profiles.size()) {
emit launchError(tr("Запуск уже отменён"));
return;
}
if (code.trimmed().isEmpty()) {
emit launchError(tr("Введите код двухфакторной аутентификации"));
return;
}
2026-08-22 18:19:13 +03:00
setBusy(true);
const Profile &profile = m_profiles[m_pendingProfile];
m_auth->loginElyByWithTotp(profile.login,
profile.password,
code.trimmed(),
profile.clientToken,
[this](const AuthResult &result) {
if (m_pendingProfile < 0)
return;
if (!result.ok) {
failLaunch(result.error);
return;
}
continueLaunch(result);
});
}
2026-08-22 18:19:13 +03:00
void LauncherBackend::cancelPendingLaunch()
{
m_pendingProfile = -1;
m_pendingVersion = -1;
m_pendingInjectorPath.clear();
setBusy(false);
}
void LauncherBackend::continueLaunch(const AuthResult &auth)
{
if (m_pendingProfile < 0 || m_pendingVersion < 0) {
failLaunch(tr("Запуск был отменён"));
return;
}
Profile &profile = m_profiles[m_pendingProfile];
const Version &version = m_versions[m_pendingVersion];
// Ely.by возвращает игровой ник и UUID аккаунта — их и запоминаем,
// чтобы в следующий раз обойтись без пароля.
if (!auth.accessToken.isEmpty() && profile.authType == kElyBy) {
profile.accessToken = auth.accessToken;
profile.clientToken = auth.clientToken;
if (!auth.uuid.isEmpty())
profile.uuid = auth.uuid;
if (!auth.playerName.isEmpty())
profile.resolvedName = auth.playerName;
profile.userType = auth.userType;
saveProfiles();
}
LaunchOptions options;
options.gameDir = gameDir();
options.versionId = m_pendingVersionData.id;
options.playerName = auth.playerName.isEmpty() ? profile.resolvedName : auth.playerName;
if (options.playerName.isEmpty())
options.playerName = profile.login.isEmpty() ? profile.name : profile.login;
options.uuid = auth.uuid.isEmpty() ? profile.uuid : auth.uuid;
options.accessToken = auth.accessToken;
options.clientToken = auth.clientToken;
options.userType = auth.userType;
options.javaPath = m_settings.value(QStringLiteral("javaPath")).toString();
options.minMemoryMb = settingInt(m_settings, QStringLiteral("minMemoryMb"), 512);
options.maxMemoryMb = settingInt(m_settings, QStringLiteral("maxMemoryMb"), 4096);
options.windowWidth = settingInt(m_settings, QStringLiteral("windowWidth"), 0);
options.windowHeight = settingInt(m_settings, QStringLiteral("windowHeight"), 0);
options.fullscreen = m_settings.value(QStringLiteral("fullscreen")).toBool();
options.serverAddress = version.serverUrl.trimmed();
options.launcherName = m_settings.value(QStringLiteral("launcherName")).toString();
options.launcherVersion = m_settings.value(QStringLiteral("launcherVersion")).toString();
const QString extra = m_settings.value(QStringLiteral("jvmArgs")).toString().trimmed();
if (!extra.isEmpty())
options.extraJvmArgs = extra.split(QRegularExpression(QStringLiteral("\\s+")),
Qt::SkipEmptyParts);
if (profile.authType == kElyBy)
options.authlibInjectorPath = m_pendingInjectorPath;
QString error;
const bool started = m_launcher->launch(options, m_pendingVersionData, &error);
const QString profileName = profile.name;
const QString versionName = version.name;
const QString serverUrl = version.serverUrl;
cancelPendingLaunch();
if (!started) {
emit launchError(error);
return;
}
emit gameRunningChanged();
emit launched(profileName, versionName, serverUrl);
}
void LauncherBackend::stopGame()
{
m_launcher->terminate();
}
void LauncherBackend::failLaunch(const QString &message)
{
cancelPendingLaunch();
emit launchError(message);
}
void LauncherBackend::setBusy(bool busy)
{
if (m_busy == busy)
return;
m_busy = busy;
emit busyChanged();
}
QString LauncherBackend::gameDir() const
{
const QString configured = m_settings.value(QStringLiteral("gameDir")).toString().trimmed();
2026-08-23 03:30:31 +03:00
return configured.isEmpty() ? LauncherPaths::defaultMinecraftDir()
: QDir::fromNativeSeparators(configured);
2026-08-22 18:19:13 +03:00
}
QVariantMap LauncherBackend::settings() const
{
QVariantMap result = m_settings;
result[QStringLiteral("resolvedGameDir")] = QDir::toNativeSeparators(gameDir());
return result;
}
void LauncherBackend::updateSettings(const QVariantMap &values)
{
const QString previousGameDir = gameDir();
for (auto it = values.cbegin(); it != values.cend(); ++it)
m_settings.insert(it.key(), it.value());
saveSettings();
emit settingsChanged();
if (gameDir() != previousGameDir)
emit installedVersionsChanged();
}
QStringList LauncherBackend::detectedJava() const
{
QStringList result;
const QStringList candidates = JavaLocator::findAll(gameDir());
for (const QString &candidate : candidates) {
const int major = JavaLocator::majorVersion(candidate);
result << (major > 0 ? QStringLiteral("Java %1 — %2")
.arg(major)
.arg(QDir::toNativeSeparators(candidate))
: QDir::toNativeSeparators(candidate));
}
return result;
}
2026-06-19 21:03:37 +03:00
void LauncherBackend::openMinecraftFolder()
{
2026-08-22 18:19:13 +03:00
const QString modsDir = gameDir() + QStringLiteral("/mods");
2026-06-19 21:03:37 +03:00
QDir().mkpath(modsDir);
if (!QDesktopServices::openUrl(QUrl::fromLocalFile(modsDir)))
2026-08-22 18:19:13 +03:00
emit launchError(tr("Не удалось открыть папку с модами"));
}
void LauncherBackend::openGameFolder()
{
const QString directory = gameDir();
QDir().mkpath(directory);
if (!QDesktopServices::openUrl(QUrl::fromLocalFile(directory)))
emit launchError(tr("Не удалось открыть папку игры"));
2026-06-19 21:03:37 +03:00
}
2026-08-23 03:30:31 +03:00
// Повреждённый файл не удаляем: отводим в .bak, чтобы данные можно было спасти
// руками, а ensureDataFiles() создал на его месте пустой.
bool LauncherBackend::moveAsideBroken(const QString &path, const QString &reason)
{
const QString backup = path + QStringLiteral(".bak");
QFile::remove(backup);
if (!QFile::rename(path, backup)) {
m_storageIssues << tr("Файл %1 повреждён (%2), и его не удалось отложить в сторону")
.arg(QDir::toNativeSeparators(path), reason);
return false;
}
m_storageIssues << tr("Файл %1 повреждён (%2). Копия сохранена как %3, файл создан заново.")
.arg(QDir::toNativeSeparators(path),
reason,
QDir::toNativeSeparators(backup));
return true;
}
QJsonArray LauncherBackend::readArrayFile(const QString &path)
{
QFile file(path);
if (!file.exists())
return {}; // первый запуск — это не ошибка
if (!file.open(QIODevice::ReadOnly)) {
m_storageIssues << tr("Не удалось прочитать %1: %2")
.arg(QDir::toNativeSeparators(path), file.errorString());
return {};
}
QJsonParseError parseError;
const QJsonDocument document = QJsonDocument::fromJson(file.readAll(), &parseError);
if (parseError.error != QJsonParseError::NoError) {
file.close();
moveAsideBroken(path, parseError.errorString());
return {};
}
if (!document.isArray()) {
file.close();
moveAsideBroken(path, tr("ожидался список записей"));
return {};
}
return document.array();
}
QJsonObject LauncherBackend::readObjectFile(const QString &path)
{
QFile file(path);
if (!file.exists())
return {};
if (!file.open(QIODevice::ReadOnly)) {
m_storageIssues << tr("Не удалось прочитать %1: %2")
.arg(QDir::toNativeSeparators(path), file.errorString());
return {};
}
QJsonParseError parseError;
const QJsonDocument document = QJsonDocument::fromJson(file.readAll(), &parseError);
if (parseError.error != QJsonParseError::NoError) {
file.close();
moveAsideBroken(path, parseError.errorString());
return {};
}
if (!document.isObject()) {
file.close();
moveAsideBroken(path, tr("ожидался набор настроек"));
return {};
}
return document.object();
}
// Перед каждой записью убеждаемся, что папка лаунчера на месте: её могли удалить
// уже после запуска, и тогда данные молча пропадали бы.
bool LauncherBackend::writeJsonFile(const QString &path, const QJsonDocument &document)
{
QString storageError;
if (!LauncherPaths::ensureRootExists(&storageError)) {
emit launchError(tr("Не удалось создать папку лаунчера: %1").arg(storageError));
return false;
}
const QByteArray payload = document.toJson();
QFile file(path);
if (!file.open(QIODevice::WriteOnly) || file.write(payload) != payload.size()) {
emit launchError(tr("Не удалось сохранить %1: %2")
.arg(QDir::toNativeSeparators(path), file.errorString()));
return false;
}
return true;
}
void LauncherBackend::loadData()
{
2026-08-23 03:30:31 +03:00
auto loadFile = [this](const QString &path, auto handler) {
for (const auto &val : readArrayFile(path))
handler(val.toObject());
};
2026-08-23 03:30:31 +03:00
loadFile(LauncherPaths::profilesFile(), [this](const QJsonObject &o) {
2026-08-22 18:19:13 +03:00
Profile profile;
profile.id = o["id"].toInt();
profile.name = o["name"].toString();
profile.login = o["login"].toString();
profile.password = o["password"].toString();
profile.authType = o.value("authType").toString(kOffline);
profile.clientToken = o.value("clientToken").toString();
profile.accessToken = o.value("accessToken").toString();
profile.uuid = o.value("uuid").toString();
profile.resolvedName = o.value("resolvedName").toString();
profile.userType = o.value("userType").toString();
if (profile.clientToken.isEmpty())
profile.clientToken = AuthService::generateClientToken();
m_profiles.append(profile);
if (profile.id >= m_nextProfileId)
m_nextProfileId = profile.id + 1;
});
2026-08-22 23:39:46 +00:00
std::sort(m_profiles.begin(), m_profiles.end(),
[](const Profile &a, const Profile &b) { return a.id < b.id; });
2026-08-23 03:30:31 +03:00
int skippedVersions = 0;
loadFile(LauncherPaths::versionsFile(), [this, &skippedVersions](const QJsonObject &o) {
2026-08-22 18:19:13 +03:00
const int id = o["id"].toInt();
2026-08-23 03:30:31 +03:00
const QString name = o["name"].toString();
const QString versionId = o.value("versionId").toString();
// Без имени и без ID запись нерабочая: versionIdOf() вернёт пустую строку.
if (name.trimmed().isEmpty() && versionId.trimmed().isEmpty()) {
++skippedVersions;
return;
}
m_versions.append({id, name, o["serverUrl"].toString(), versionId});
if (id >= m_nextVersionId)
m_nextVersionId = id + 1;
});
2026-08-23 03:30:31 +03:00
if (skippedVersions > 0) {
m_storageIssues << tr("В %1 пропущены записи без имени и ID версии: %2")
.arg(QDir::toNativeSeparators(LauncherPaths::versionsFile()))
.arg(skippedVersions);
}
2026-08-22 23:39:46 +00:00
std::sort(m_versions.begin(), m_versions.end(),
[](const Version &a, const Version &b) { return a.id < b.id; });
}
2026-08-23 03:30:31 +03:00
// Файлы могли не появиться при первом запуске, быть удалены или отложены в .bak —
// создаём заново, чтобы папка лаунчера всегда была укомплектована.
void LauncherBackend::ensureDataFiles()
{
if (!QFile::exists(LauncherPaths::settingsFile()))
saveSettings();
if (!QFile::exists(LauncherPaths::profilesFile()))
saveProfiles();
if (!QFile::exists(LauncherPaths::versionsFile()))
saveVersions();
}
void LauncherBackend::saveProfiles()
{
QJsonArray arr;
for (const auto &p : m_profiles)
2026-08-22 18:19:13 +03:00
arr.append(QJsonObject{{"id", p.id},
{"name", p.name},
{"login", p.login},
{"password", p.password},
{"authType", p.authType},
{"clientToken", p.clientToken},
{"accessToken", p.accessToken},
{"uuid", p.uuid},
{"resolvedName", p.resolvedName},
{"userType", p.userType}});
2026-08-23 03:30:31 +03:00
writeJsonFile(LauncherPaths::profilesFile(), QJsonDocument(arr));
}
void LauncherBackend::saveVersions()
{
QJsonArray arr;
for (const auto &v : m_versions)
2026-08-22 18:19:13 +03:00
arr.append(QJsonObject{{"id", v.id},
{"name", v.name},
{"serverUrl", v.serverUrl},
{"versionId", v.versionId}});
2026-08-23 03:30:31 +03:00
writeJsonFile(LauncherPaths::versionsFile(), QJsonDocument(arr));
}
2026-08-22 18:19:13 +03:00
void LauncherBackend::loadSettings()
{
m_settings = {{QStringLiteral("gameDir"), QString()},
{QStringLiteral("javaPath"), QString()},
{QStringLiteral("minMemoryMb"), 512},
{QStringLiteral("maxMemoryMb"), 4096},
{QStringLiteral("jvmArgs"), QString()},
{QStringLiteral("windowWidth"), 0},
{QStringLiteral("windowHeight"), 0},
{QStringLiteral("fullscreen"), false},
{QStringLiteral("launcherName"), QStringLiteral("KishkaLauncher")},
{QStringLiteral("launcherVersion"), QStringLiteral("1.0")}};
2026-08-23 03:30:31 +03:00
const QJsonObject stored = readObjectFile(LauncherPaths::settingsFile());
2026-08-22 18:19:13 +03:00
for (auto it = stored.constBegin(); it != stored.constEnd(); ++it)
m_settings.insert(it.key(), it.value().toVariant());
}
void LauncherBackend::saveSettings()
{
2026-08-23 03:30:31 +03:00
writeJsonFile(LauncherPaths::settingsFile(),
QJsonDocument(QJsonObject::fromVariantMap(m_settings)));
2026-08-22 18:19:13 +03:00
}