Files
minecraft-launcher/mainwindow.cpp

468 lines
19 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "mainwindow.h"
#include "ui_mainwindow.h" // Подключаем сгенерированный заголовочный файл
#include "settingsdialog.h"
#include "profiledialog.h" // Подключаем новый диалог
#include <QMessageBox>
#include <QDesktopServices>
#include <QUrl>
#include <QDir>
#include <QInputDialog> // Для диалога выбора
#include <QFileInfo> // Для проверки файла
#include <QJsonDocument> // Для работы с JSON
#include <QJsonObject> // Для работы с JSON
#include <QJsonArray> // Для работы с JSON
#include <QJsonValue> // Для работы с JSON
#include <QUuid> // Для генерации ID профиля
#include <QStandardPaths> // Для поиска папки с данными
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow) // Инициализируем указатель на UI
{
ui->setupUi(this); // Загружаем интерфейс из .ui файла
// Начальная настройка виджетов
ui->progressBar->setRange(0, 0); // Неопределенный прогресс-бар
ui->progressBar->setVisible(false);
// Инициализация логики
settings = new QSettings("GaleonDev", "MinecraftLauncher", this);
process = new QProcess(this);
// Подключаем сигналы от процесса (это делается вручную)
connect(process, &QProcess::readyReadStandardOutput, this, &MainWindow::readProcessOutput);
connect(process, &QProcess::readyReadStandardError, this, &MainWindow::readProcessOutput);
connect(process, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(onProcessFinished(int, QProcess::ExitStatus)));
connect(process, SIGNAL(errorOccurred(QProcess::ProcessError)), this, SLOT(onProcessError(QProcess::ProcessError)));
// Загружаем профили при старте
profilesPath = "/launcher_profiles.json";
loadProfiles();
}
MainWindow::~MainWindow()
{
delete ui; // Освобождаем память от объекта UI
}
// Вспомогательная функция для проверки правил OS
bool checkRules(const QJsonObject &item) {
if (!item.contains("rules")) {
return true; // Нет правил - разрешено для всех.
}
// Если правила есть, по умолчанию запрещаем, пока не найдем разрешающее правило.
bool isAllowed = false;
QString currentOs;
#if defined(Q_OS_WIN)
currentOs = "windows";
#elif defined(Q_OS_MAC)
currentOs = "osx";
#elif defined(Q_OS_LINUX)
currentOs = "linux";
#endif
QJsonArray rules = item["rules"].toArray();
for (const QJsonValue &value : rules) {
QJsonObject rule = value.toObject();
QString action = rule["action"].toString();
bool conditionMet = false;
if (rule.contains("os")) {
QJsonObject osRule = rule["os"].toObject();
if (osRule.contains("name") && osRule["name"].toString() == currentOs) {
conditionMet = true;
}
} else {
// Правило без указания ОС применяется ко всем системам.
conditionMet = true;
}
if (conditionMet) {
if (action == "allow") {
isAllowed = true;
} else if (action == "disallow") {
isAllowed = false;
}
}
}
return isAllowed;
}
void MainWindow::loadProfiles()
{
QFile profilesFile(profilesPath);
if (profilesFile.exists() && profilesFile.open(QIODevice::ReadOnly)) {
QJsonDocument doc = QJsonDocument::fromJson(profilesFile.readAll());
profilesData = doc.object();
profilesFile.close();
} else {
// Создаем базовую структуру, если файла нет
profilesData = QJsonObject({{"profiles", QJsonObject()}});
}
QJsonObject profiles = profilesData["profiles"].toObject();
ui->profileComboBox->clear();
if (profiles.isEmpty()) {
// Если профилей нет, принудительно открываем диалог создания
QMessageBox::information(this, "Настройка", "Не найдено ни одного профиля. Давайте создадим новый.");
createNewProfile();
return; // createNewProfile вызовет loadProfiles() повторно
}
// Заполняем ComboBox
for (const QString &key : profiles.keys()) {
QJsonObject profile = profiles[key].toObject();
ui->profileComboBox->addItem(profile["name"].toString(), key); // Имя и UUID
}
}
void MainWindow::saveProfiles()
{
QFile profilesFile(profilesPath);
if (profilesFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
QJsonDocument doc(profilesData);
profilesFile.write(doc.toJson(QJsonDocument::Indented));
profilesFile.close();
} else {
QMessageBox::critical(this, "Ошибка", "Не удалось сохранить файл профилей: " + profilesFile.errorString());
}
}
void MainWindow::on_editProfileButton_clicked()
{
// 1. Проверяем, выбран ли профиль
QString currentProfileId = ui->profileComboBox->currentData().toString();
if (currentProfileId.isEmpty()) {
QMessageBox::warning(this, "Нет профиля", "Пожалуйста, выберите профиль для редактирования.");
return;
}
// 2. Получаем данные текущего профиля
QJsonObject profiles = profilesData["profiles"].toObject();
QJsonObject currentProfile = profiles[currentProfileId].toObject();
// 3. Создаем диалог и заполняем его данными
ProfileDialog dialog(this);
dialog.setWindowTitle("Редактирование профиля");
dialog.setProfileName(currentProfile["name"].toString());
dialog.setJavaPath(currentProfile["javaDir"].toString());
// 4. Показываем диалог и обрабатываем результат
if (dialog.exec() == QDialog::Accepted) {
if (dialog.profileName().isEmpty() || dialog.javaPath().isEmpty()) {
QMessageBox::warning(this, "Ошибка", "Имя профиля и путь к Java не могут быть пустыми.");
return;
}
// 5. Обновляем данные в нашем JSON объекте
currentProfile["name"] = dialog.profileName();
currentProfile["javaDir"] = dialog.javaPath();
// 6. Записываем обновленный профиль обратно
profiles[currentProfileId] = currentProfile;
profilesData["profiles"] = profiles;
// 7. Сохраняем и перезагружаем список
saveProfiles();
// Сохраняем ID, чтобы восстановить выбор после перезагрузки
QString previouslySelectedId = currentProfileId;
loadProfiles();
int indexToSelect = ui->profileComboBox->findData(previouslySelectedId);
if (indexToSelect != -1) {
ui->profileComboBox->setCurrentIndex(indexToSelect);
}
}
}
void MainWindow::createNewProfile()
{
ProfileDialog dialog(this);
if (dialog.exec() == QDialog::Accepted) {
if (dialog.profileName().isEmpty() || dialog.javaPath().isEmpty()) {
QMessageBox::warning(this, "Ошибка", "Имя профиля и путь к Java не могут быть пустыми.");
return;
}
QString uuid = QUuid::createUuid().toString(QUuid::WithoutBraces);
QJsonObject newProfile;
newProfile["name"] = dialog.profileName();
newProfile["javaDir"] = dialog.javaPath();
newProfile["lastVersionId"] = "";
newProfile["created"] = QDateTime::currentDateTime().toString(Qt::ISODate);
newProfile["lastUsed"] = QDateTime::currentDateTime().toString(Qt::ISODate);
newProfile["icon"] = "Bedrock";
newProfile["type"] = "custom";
QJsonObject profiles = profilesData["profiles"].toObject();
profiles[uuid] = newProfile;
profilesData["profiles"] = profiles;
saveProfiles();
loadProfiles(); // Перезагружаем список профилей
}
}
void MainWindow::on_addProfileButton_clicked()
{
createNewProfile();
}
void MainWindow::on_profileComboBox_currentIndexChanged(int index)
{
// Пока ничего не делаем, но слот полезен для будущего
Q_UNUSED(index);
}
void MainWindow::on_launchButton_clicked()
{
QString currentProfileId = ui->profileComboBox->currentData().toString();
if (currentProfileId.isEmpty()) {
QMessageBox::critical(this, "Ошибка", "Не выбран профиль для запуска!");
return;
}
QJsonObject profile = profilesData["profiles"].toObject()[currentProfileId].toObject();
QString javaPath = profile["javaDir"].toString();
if (!QFileInfo::exists(javaPath)) {
QMessageBox::critical(this, "Ошибка", "Не найден исполняемый файл Java, указанный в профиле:\n" + javaPath);
return;
}
// --- 1. Выбор версии (остается без изменений) ---
QString minecraftPath = getMinecraftPath();
if (minecraftPath.isEmpty()) {
QMessageBox::warning(this, "Ошибка", "Не удалось найти директорию .minecraft.");
return;
}
QString versionsPath = QDir::toNativeSeparators(minecraftPath + "/versions");
QDir versionsDir(versionsPath);
if (!versionsDir.exists()) {
QMessageBox::warning(this, "Ошибка", "Папка 'versions' не найдена!");
return;
}
QStringList versionList = versionsDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
if (versionList.isEmpty()) {
QMessageBox::warning(this, "Ошибка", "Не найдено ни одной установленной версии Minecraft.");
return;
}
bool ok;
QString selectedVersion = QInputDialog::getItem(this, "Выбор версии",
"Выберите версию Minecraft для запуска:",
versionList, 0, false, &ok);
if (!ok || selectedVersion.isEmpty()) {
return; // Пользователь отменил выбор
}
// --- 2. Чтение и парсинг JSON файла версии ---
QString jsonPath = QDir::toNativeSeparators(versionsPath + "/" + selectedVersion + "/" + selectedVersion + ".json");
QFile jsonFile(jsonPath);
if (!jsonFile.open(QIODevice::ReadOnly)) {
QMessageBox::critical(this, "Ошибка", "Не удалось открыть .json файл для версии " + selectedVersion);
return;
}
QByteArray jsonData = jsonFile.readAll();
jsonFile.close();
QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData);
if (jsonDoc.isNull()) {
QMessageBox::critical(this, "Ошибка", "Некорректный .json файл для версии " + selectedVersion);
return;
}
QJsonObject rootObj = jsonDoc.object();
// --- 3. Формирование Classpath ---
QStringList classpathEntries;
QString librariesPath = QDir::toNativeSeparators(minecraftPath + "/libraries");
QJsonArray libraries = rootObj["libraries"].toArray();
for (const QJsonValue &value : libraries) {
QJsonObject library = value.toObject();
if (!checkRules(library)) {
continue; // Пропускаем библиотеку, если она не для этой ОС
}
QJsonObject downloads = library["downloads"].toObject();
if(downloads.isEmpty()){ // Для Fabric Loader и некоторых других
QString name = library["name"].toString();
QStringList parts = name.split(':');
QString path = parts[0].replace('.', '/') + "/" + parts[1] + "/" + parts[2] + "/" + parts[1] + "-" + parts[2] + ".jar";
classpathEntries << QDir::toNativeSeparators(librariesPath + "/" + path);
} else {
QJsonObject artifact = downloads["artifact"].toObject();
QString path = artifact["path"].toString();
classpathEntries << QDir::toNativeSeparators(librariesPath + "/" + path);
}
}
// Добавляем сам .jar файл игры в classpath
classpathEntries << QDir::toNativeSeparators(versionsPath + "/" + selectedVersion + "/" + selectedVersion + ".jar");
// Определяем разделитель для classpath в зависимости от ОС
QString pathSeparator;
#if defined(Q_OS_WIN)
pathSeparator = ";";
#else
pathSeparator = ":";
#endif
QString classpath = classpathEntries.join(pathSeparator);
// --- 4. Сборка аргументов ---
QStringList jvmArgs;
QStringList gameArgs;
// Аргументы для JVM
QJsonObject argumentsObj = rootObj["arguments"].toObject();
QJsonArray jvmArgsArray = argumentsObj["jvm"].toArray();
for (const QJsonValue &value : jvmArgsArray) {
if (value.isString()) {
jvmArgs << value.toString();
} else if (value.isObject()) {
if (checkRules(value.toObject())) {
QJsonArray values = value.toObject()["values"].toArray();
for(const QJsonValue &v : values){
jvmArgs << v.toString();
}
}
}
}
// Аргументы для игры
QJsonArray gameArgsArray = argumentsObj["game"].toArray();
for (const QJsonValue &value : gameArgsArray) {
if (value.isString()) {
gameArgs << value.toString();
} else if (value.isObject()) {
if (checkRules(value.toObject())) {
QJsonArray values = value.toObject()["values"].toArray();
for(const QJsonValue &v : values){
gameArgs << v.toString();
}
}
}
}
// --- 5. Подстановка значений в плейсхолдеры ---
QString nativesPath = QDir::toNativeSeparators(versionsPath + "/" + selectedVersion + "/natives");
QDir(nativesPath).mkpath("."); // Создаем папку для нативных библиотек, если ее нет
// Заменители для JVM аргументов
for (QString &arg : jvmArgs) {
arg.replace("${natives_directory}", nativesPath);
arg.replace("${launcher_name}", "CustomLauncher");
arg.replace("${launcher_version}", "1.0");
arg.replace("${classpath}", classpath);
}
// Заменители для игровых аргументов (используем базовые значения)
for (QString &arg : gameArgs) {
arg.replace("${auth_player_name}", "Player");
arg.replace("${version_name}", selectedVersion);
arg.replace("${game_directory}", minecraftPath);
arg.replace("${assets_root}", QDir::toNativeSeparators(minecraftPath + "/assets"));
arg.replace("${assets_index_name}", rootObj["assetIndex"].toObject()["id"].toString());
arg.replace("${auth_uuid}", "00000000-0000-0000-0000-000000000000");
arg.replace("${auth_access_token}", "0");
arg.replace("${clientid}", "N/A");
arg.replace("${auth_xuid}", "N/A");
arg.replace("${user_type}", "msa");
arg.replace("${version_type}", rootObj["type"].toString());
}
// --- 6. Финальная сборка и запуск ---
QString mainClass = rootObj["mainClass"].toString();
//QString javaPath = "java";
QStringList finalArguments;
finalArguments << jvmArgs << mainClass << gameArgs;
ui->logOutput->appendPlainText("Запуск Minecraft версии: " + selectedVersion);
ui->logOutput->appendPlainText("Главный класс: " + mainClass);
// Для отладки можно вывести всю команду
ui->logOutput->appendPlainText("Команда: " + javaPath + " " + finalArguments.join(" "));
process->start(javaPath, finalArguments);
ui->progressBar->setVisible(true);
}
void MainWindow::on_modsFolderButton_clicked()
{
QString minecraftPath = getMinecraftPath();
if (minecraftPath.isEmpty()) {
QMessageBox::warning(this, "Ошибка", "Не удалось найти директорию .minecraft.");
return;
}
QString modsPath = minecraftPath + "/mods";
QDesktopServices::openUrl(QUrl::fromLocalFile(modsPath));
}
void MainWindow::on_settingsButton_clicked()
{
SettingsDialog dialog(this);
dialog.exec();
}
void MainWindow::on_updateModsButton_clicked()
{
QString gitRepoUrl = settings->value("gitRepoUrl").toString();
if (gitRepoUrl.isEmpty()) {
QMessageBox::information(this, "Настройки", "Пожалуйста, укажите URL Git-репозитория в настройках.");
return;
}
QString minecraftPath = getMinecraftPath();
if (minecraftPath.isEmpty()) {
QMessageBox::warning(this, "Ошибка", "Не удалось найти директорию .minecraft.");
return;
}
QString modsPath = minecraftPath + "/mods";
QDir modsDir(modsPath);
if (!modsDir.exists(".git")) {
ui->logOutput->appendPlainText("Клонирование модов из " + gitRepoUrl);
process->start("git", QStringList() << "clone" << gitRepoUrl << modsPath);
} else {
ui->logOutput->appendPlainText("Обновление модов...");
process->setWorkingDirectory(modsPath);
process->start("git", QStringList() << "pull");
}
ui->progressBar->setVisible(true);
}
void MainWindow::onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
ui->progressBar->setVisible(false);
if (exitStatus == QProcess::CrashExit) {
ui->logOutput->appendPlainText("Процесс завершился с ошибкой.");
} else {
ui->logOutput->appendPlainText("Процесс успешно завершен с кодом " + QString::number(exitCode));
}
}
void MainWindow::onProcessError(QProcess::ProcessError error)
{
ui->progressBar->setVisible(false);
ui->logOutput->appendPlainText("Ошибка запуска процесса: " + process->errorString());
}
void MainWindow::readProcessOutput()
{
ui->logOutput->appendPlainText(process->readAllStandardOutput());
ui->logOutput->appendPlainText(process->readAllStandardError());
}
QString MainWindow::getMinecraftPath()
{
QString path;
#if defined(Q_OS_WIN)
path = QDir::homePath() + "/AppData/Roaming/.minecraft";
#elif defined(Q_OS_MAC)
path = QDir::homePath() + "/Library/Application Support/minecraft";
#else
path = QDir::homePath() + "/.minecraft";
#endif
return path;
}