diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index f429b87..984594d 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -17,6 +17,7 @@ target_sources(astra PRIVATE include/squareboot.h include/squarelauncher.h include/steamapi.h + include/utility.h src/account.cpp src/accountmanager.cpp @@ -32,6 +33,7 @@ target_sources(astra PRIVATE src/squareboot.cpp src/squarelauncher.cpp src/steamapi.cpp + src/utility.cpp resources.qrc) kconfig_add_kcfg_files(astra GENERATE_MOC config.kcfgc accountconfig.kcfgc profileconfig.kcfgc) diff --git a/launcher/config.kcfg b/launcher/config.kcfg index 0bb1937..07abb68 100644 --- a/launcher/config.kcfg +++ b/launcher/config.kcfg @@ -17,5 +17,8 @@ SPDX-License-Identifier: CC0-1.0 + + false + diff --git a/launcher/include/account.h b/launcher/include/account.h index 56d5979..d474887 100644 --- a/launcher/include/account.h +++ b/launcher/include/account.h @@ -3,6 +3,7 @@ #pragma once +#include #include #include "accountconfig.h" @@ -66,6 +67,8 @@ public: Q_INVOKABLE QString getOTP(); + QDir getConfigDir() const; + Q_SIGNALS: void nameChanged(); void lodestoneIdChanged(); diff --git a/launcher/include/assetupdater.h b/launcher/include/assetupdater.h index ca1e441..5bc4f27 100644 --- a/launcher/include/assetupdater.h +++ b/launcher/include/assetupdater.h @@ -37,6 +37,11 @@ private: QString remoteRuntimeVersion; QTemporaryDir tempDir; + QDir dataDir; + QDir appDataDir; + QDir dalamudDir; + QDir dalamudAssetDir; + QDir dalamudRuntimeDir; bool doneDownloadingDalamud = false; bool doneDownloadingRuntimeCore = false; @@ -48,6 +53,5 @@ private: QList dalamudAssetNeededFilenames; QJsonArray remoteDalamudAssetArray; - QString dataDir; Profile &m_profile; }; diff --git a/launcher/include/launchercore.h b/launcher/include/launchercore.h index c096ca0..bade3b3 100755 --- a/launcher/include/launchercore.h +++ b/launcher/include/launchercore.h @@ -63,6 +63,7 @@ class LauncherCore : public QObject Q_PROPERTY(AccountManager *accountManager READ accountManager CONSTANT) Q_PROPERTY(bool closeWhenLaunched READ closeWhenLaunched WRITE setCloseWhenLaunched NOTIFY closeWhenLaunchedChanged) Q_PROPERTY(bool showNews READ showNews WRITE setShowNews NOTIFY showNewsChanged) + Q_PROPERTY(bool keepPatches READ keepPatches WRITE setKeepPatches NOTIFY keepPatchesChanged) Q_PROPERTY(Headline *headline READ headline NOTIFY newsChanged) public: @@ -120,6 +121,9 @@ public: bool showNews() const; void setShowNews(bool value); + bool keepPatches() const; + void setKeepPatches(bool value); + int defaultProfileIndex = 0; bool m_isSteam = false; @@ -147,6 +151,7 @@ signals: void gameClosed(); void closeWhenLaunchedChanged(); void showNewsChanged(); + void keepPatchesChanged(); void loginError(QString message); void stageChanged(QString message); void stageIndeterminate(); diff --git a/launcher/include/patcher.h b/launcher/include/patcher.h index b77e188..b1a62b8 100644 --- a/launcher/include/patcher.h +++ b/launcher/include/patcher.h @@ -3,6 +3,7 @@ #pragma once +#include #include #include #include @@ -25,6 +26,7 @@ signals: private: void checkIfDone(); + void setupDirectories(); [[nodiscard]] bool isBoot() const { @@ -42,6 +44,7 @@ private: QVector patchQueue; + QDir patchesDir; QString baseDirectory; BootData *boot_data = nullptr; GameData *game_data = nullptr; diff --git a/launcher/include/profile.h b/launcher/include/profile.h index 8fb2f47..64a0af8 100644 --- a/launcher/include/profile.h +++ b/launcher/include/profile.h @@ -150,6 +150,8 @@ public: return !m_wineVersion.isEmpty(); } + QString dalamudChannelName() const; + Q_SIGNALS: void gameInstallChanged(); void nameChanged(); diff --git a/launcher/include/profilemanager.h b/launcher/include/profilemanager.h index 7dc479b..98654e2 100644 --- a/launcher/include/profilemanager.h +++ b/launcher/include/profilemanager.h @@ -36,7 +36,7 @@ public: Q_INVOKABLE bool canDelete(Profile *account) const; - static QString getDefaultGamePath(); + static QString getDefaultGamePath(const QString &uuid); private: void insertProfile(Profile *profile); diff --git a/launcher/include/utility.h b/launcher/include/utility.h new file mode 100644 index 0000000..6ab684f --- /dev/null +++ b/launcher/include/utility.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +namespace Utility +{ +QDir stateDirectory(); +QString toWindowsPath(const QDir &dir); +} \ No newline at end of file diff --git a/launcher/profileconfig.kcfg b/launcher/profileconfig.kcfg index ecd9c71..9d08887 100644 --- a/launcher/profileconfig.kcfg +++ b/launcher/profileconfig.kcfg @@ -20,7 +20,7 @@ SPDX-License-Identifier: CC0-1.0 1 - ProfileManager::getDefaultGamePath() + ProfileManager::getDefaultGamePath(mParamuuid) diff --git a/launcher/src/account.cpp b/launcher/src/account.cpp index 5c1eee9..e060279 100644 --- a/launcher/src/account.cpp +++ b/launcher/src/account.cpp @@ -178,6 +178,13 @@ QString Account::getOTP() return totpStr; } +QDir Account::getConfigDir() const +{ + const QDir dataDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + const QDir userDir = dataDir.absoluteFilePath("user"); + return userDir.absoluteFilePath(m_key); +} + void Account::fetchAvatar() { if (lodestoneId().isEmpty()) { diff --git a/launcher/src/assetupdater.cpp b/launcher/src/assetupdater.cpp index 6b21ce6..b1071be 100644 --- a/launcher/src/assetupdater.cpp +++ b/launcher/src/assetupdater.cpp @@ -35,9 +35,19 @@ AssetUpdater::AssetUpdater(Profile &profile, LauncherCore &launcher, QObject *pa launcher.mgr->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy); dataDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + dalamudDir = dataDir.absoluteFilePath("dalamud"); + dalamudAssetDir = dalamudDir.absoluteFilePath("assets"); + dalamudRuntimeDir = dalamudDir.absoluteFilePath("runtime"); - if (!QDir().exists(dataDir)) - QDir().mkdir(dataDir); + const auto createIfNeeded = [](const QDir &dir) { + if (!QDir().exists(dir.absolutePath())) + QDir().mkdir(dir.absolutePath()); + }; + + createIfNeeded(dataDir); + createIfNeeded(dalamudDir); + createIfNeeded(dalamudAssetDir); + createIfNeeded(dalamudRuntimeDir); } void AssetUpdater::update() @@ -124,7 +134,7 @@ void AssetUpdater::update() void AssetUpdater::beginInstall() { if (needsDalamudInstall) { - bool success = !JlCompress::extractDir(tempDir.path() + "/latest.zip", dataDir + "/Dalamud").empty(); + bool success = !JlCompress::extractDir(tempDir.path() + "/latest.zip", dalamudDir.absoluteFilePath(m_profile.dalamudChannelName())).empty(); if (!success) { // TODO: handle failure here @@ -135,14 +145,14 @@ void AssetUpdater::beginInstall() } if (needsRuntimeInstall) { - bool success = !JlCompress::extractDir(tempDir.path() + "/dotnet-core.zip", dataDir + "/DalamudRuntime").empty(); + bool success = !JlCompress::extractDir(tempDir.path() + "/dotnet-core.zip", dalamudRuntimeDir.absolutePath()).empty(); - success |= !JlCompress::extractDir(tempDir.path() + "/dotnet-desktop.zip", dataDir + "/DalamudRuntime").empty(); + success |= !JlCompress::extractDir(tempDir.path() + "/dotnet-desktop.zip", dalamudRuntimeDir.absolutePath()).empty(); if (!success) { qInfo() << "Failed to install dotnet!"; } else { - QFile file(dataDir + "/DalamudRuntime/runtime.ver"); + QFile file(dalamudRuntimeDir.absoluteFilePath("runtime.ver")); file.open(QIODevice::WriteOnly | QIODevice::Text); file.write(remoteRuntimeVersion.toUtf8()); file.close(); @@ -161,7 +171,7 @@ void AssetUpdater::checkIfDalamudAssetsDone() m_profile.dalamudAssetVersion = remoteDalamudAssetVersion; - QFile file(dataDir + "/DalamudAssets/" + "asset.ver"); + QFile file(dalamudAssetDir.absoluteFilePath("asset.ver")); file.open(QIODevice::WriteOnly | QIODevice::Text); file.write(QString::number(remoteDalamudAssetVersion).toUtf8()); file.close(); @@ -294,21 +304,15 @@ void AssetUpdater::checkIfCheckingIsDone() auto assetReply = launcher.mgr->get(assetRequest); connect(assetReply, &QNetworkReply::finished, [this, assetReply, assetObject = assetObject.toObject()] { - if (!QDir().exists(dataDir + "/DalamudAssets")) - QDir().mkdir(dataDir + "/DalamudAssets"); - const QString fileName = assetObject["FileName"].toString(); - const QList dirPath = fileName.left(fileName.lastIndexOf("/")).split('/'); + const QString dirPath = fileName.left(fileName.lastIndexOf("/")); - QString build = dataDir + "/DalamudAssets/"; - for (const auto &dir : dirPath) { - if (!QDir().exists(build + dir)) - QDir().mkdir(build + dir); + const QString path = dalamudAssetDir.absoluteFilePath(dirPath); - build += dir + "/"; - } + if (!QDir().exists(path)) + QDir().mkpath(path); - QFile file(dataDir + "/DalamudAssets/" + assetObject["FileName"].toString()); + QFile file(dalamudAssetDir.absoluteFilePath(assetObject["FileName"].toString())); file.open(QIODevice::WriteOnly); file.write(assetReply->readAll()); file.close(); diff --git a/launcher/src/launchercore.cpp b/launcher/src/launchercore.cpp index d8f9235..0483df1 100755 --- a/launcher/src/launchercore.cpp +++ b/launcher/src/launchercore.cpp @@ -21,6 +21,7 @@ #include "launchercore.h" #include "sapphirelauncher.h" #include "squarelauncher.h" +#include "utility.h" #ifdef ENABLE_WATCHDOG #include "watchdog.h" @@ -103,11 +104,23 @@ void LauncherCore::beginVanillaGame(const QString &gameExecutablePath, const Pro void LauncherCore::beginDalamudGame(const QString &gameExecutablePath, const Profile &profile, const LoginAuth &auth) { - QString gamePath = gameExecutablePath; - gamePath = "Z:" + gamePath.replace('/', '\\'); + const QDir dataDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + const QDir configDir = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); + const QDir stateDir = Utility::stateDirectory(); - QString dataDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); - dataDir = "Z:" + dataDir.replace('/', '\\'); + const QString logDir = stateDir.absoluteFilePath("logs"); + + if (!QDir().exists(logDir)) + QDir().mkpath(logDir); + + const QDir dalamudDir = dataDir.absoluteFilePath("dalamud"); + const QDir dalamudRuntimeDir = dalamudDir.absoluteFilePath("runtime"); + const QDir dalamudAssetDir = dalamudDir.absoluteFilePath("assets"); + const QDir dalamudConfigPath = configDir.absoluteFilePath("dalamud-config.json"); + const QDir dalamudPluginDir = dalamudDir.absoluteFilePath("plugins"); + + const QDir dalamudInstallDir = dalamudDir.absoluteFilePath(profile.dalamudChannelName()); + const QString dalamudInjector = dalamudInstallDir.absoluteFilePath("Dalamud.Injector.exe"); auto dalamudProcess = new QProcess(this); connect(dalamudProcess, qOverload(&QProcess::finished), this, [this](int exitCode) { @@ -116,7 +129,7 @@ void LauncherCore::beginDalamudGame(const QString &gameExecutablePath, const Pro }); QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); - env.insert("DALAMUD_RUNTIME", dataDir + "\\DalamudRuntime"); + env.insert("DALAMUD_RUNTIME", Utility::toWindowsPath(dalamudRuntimeDir)); #if defined(Q_OS_LINUX) || defined(Q_OS_MAC) env.insert("XL_WINEONLINUX", "true"); @@ -127,16 +140,17 @@ void LauncherCore::beginDalamudGame(const QString &gameExecutablePath, const Pro launchExecutable(profile, dalamudProcess, - {dataDir + "/Dalamud/" + "Dalamud.Injector.exe", + {Utility::toWindowsPath(dalamudInjector), "launch", "-m", "inject", - "--game=" + gamePath, - "--dalamud-configuration-path=" + dataDir + "\\dalamudConfig.json", - "--dalamud-plugin-directory=" + dataDir + "\\installedPlugins", - "--dalamud-asset-directory=" + dataDir + "\\DalamudAssets", + "--game=" + Utility::toWindowsPath(gameExecutablePath), + "--dalamud-working-directory=" + Utility::toWindowsPath(dalamudDir), + "--dalamud-configuration-path=" + Utility::toWindowsPath(dalamudConfigPath), + "--dalamud-plugin-directory=" + Utility::toWindowsPath(dalamudPluginDir), + "--dalamud-asset-directory=" + Utility::toWindowsPath(dalamudAssetDir), "--dalamud-client-language=" + QString::number(profile.language()), - "--logpath=" + dataDir, + "--logpath=" + Utility::toWindowsPath(logDir), "--", args}, true, @@ -158,6 +172,12 @@ QString LauncherCore::getGameArgs(const Profile &profile, const LoginAuth &auth) gameArgs.push_back({"SYS.Region", QString::number(auth.region)}); gameArgs.push_back({"language", QString::number(profile.language())}); gameArgs.push_back({"ver", profile.repositories.repositories[0].version}); + gameArgs.push_back({"UserPath", Utility::toWindowsPath(profile.account()->getConfigDir().absolutePath())}); + + // FIXME: this should belong somewhere else... + if (!QDir().exists(profile.account()->getConfigDir().absolutePath())) { + QDir().mkpath(profile.account()->getConfigDir().absolutePath()); + } if (!auth.lobbyhost.isEmpty()) { gameArgs.push_back({"DEV.GMServerHost", auth.frontierHost}); @@ -431,6 +451,20 @@ void LauncherCore::setShowNews(const bool value) } } +bool LauncherCore::keepPatches() const +{ + return Config::keepPatches(); +} + +void LauncherCore::setKeepPatches(const bool value) +{ + if (value != Config::keepPatches()) { + Config::setKeepPatches(value); + Config::self()->save(); + Q_EMIT keepPatchesChanged(); + } +} + void LauncherCore::refreshNews() { QUrlQuery query; @@ -524,33 +558,7 @@ void LauncherCore::openOfficialLauncher(Profile *profile) executeArg = executeArg.arg(dateTime.time().hour(), 2, 10, QLatin1Char('0')); executeArg = executeArg.arg(dateTime.time().minute(), 2, 10, QLatin1Char('0')); - QList arguments; - arguments.push_back({"ExecuteArg", executeArg}); - - // find user path - QString userPath; - - // TODO: don't put this here - QString searchDir; -#if defined(Q_OS_LINUX) || defined(Q_OS_MAC) - searchDir = profile->winePrefixPath() + "/drive_c/users"; -#else - searchDir = "C:/Users"; -#endif - - QDirIterator it(searchDir); - while (it.hasNext()) { - QString dir = it.next(); - QFileInfo fi(dir); - QString fileName = fi.fileName(); - - // FIXME: is there no easier way to filter out these in Qt? - if (fi.fileName() != "Public" && fi.fileName() != "." && fi.fileName() != "..") { - userPath = fileName; - } - } - - arguments.push_back({"UserPath", QString(R"(C:\Users\%1\Documents\My Games\FINAL FANTASY XIV - A Realm Reborn)").arg(userPath)}); + QList arguments{{"ExecuteArg", executeArg}, {"UserPath", Utility::toWindowsPath(profile->account()->getConfigDir().absolutePath())}}; const QString argFormat = " /%1 =%2"; diff --git a/launcher/src/patcher.cpp b/launcher/src/patcher.cpp index eb8d140..70331df 100644 --- a/launcher/src/patcher.cpp +++ b/launcher/src/patcher.cpp @@ -21,6 +21,8 @@ Patcher::Patcher(LauncherCore &launcher, QString baseDirectory, BootData *boot_d , boot_data(boot_data) , m_launcher(launcher) { + setupDirectories(); + Q_EMIT m_launcher.stageChanged(i18n("Checking the FINAL FANTASY XIV Updater/Launcher version.")); } @@ -30,6 +32,8 @@ Patcher::Patcher(LauncherCore &launcher, QString baseDirectory, GameData *game_d , game_data(game_data) , m_launcher(launcher) { + setupDirectories(); + Q_EMIT m_launcher.stageChanged(i18n("Checking the FINAL FANTASY XIV Game version.")); } @@ -65,18 +69,18 @@ void Patcher::processPatchList(QNetworkAccessManager &mgr, const QString &patchL const QString name = version; const QStringList hashes = patchParts.size() == 9 ? (patchParts[7].split(',')) : QStringList(); const QString url = patchParts[patchParts.size() == 9 ? 8 : 5]; - - qDebug() << "Parsed patch name: " << name; + const QString filename = QStringLiteral("%1.patch").arg(name); auto url_parts = url.split('/'); const QString repository = url_parts[url_parts.size() - 3]; - const QString patchesDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/patches/" + repository; + const QDir repositoryDir = patchesDir.absoluteFilePath(repository); - if (!QDir().exists(patchesDir)) - QDir().mkpath(patchesDir); + if (!QDir().exists(repositoryDir.absolutePath())) + QDir().mkpath(repositoryDir.absolutePath()); - if (!QFile::exists(patchesDir + "/" + name + ".patch")) { + const QString patchPath = repositoryDir.absoluteFilePath(filename); + if (!QFile::exists(patchPath)) { qDebug() << "Need to download " + name; QNetworkRequest patchRequest(url); @@ -95,15 +99,13 @@ void Patcher::processPatchList(QNetworkAccessManager &mgr, const QString &patchL connect(patchReply, &QNetworkReply::finished, - [this, ourIndex, patchesDir, name, patchReply, repository, version, hashes, hashBlockSize, length] { - QFile file(patchesDir + "/" + name + ".patch"); + [this, ourIndex, patchPath, name, patchReply, repository, version, hashes, hashBlockSize, length] { + QFile file(patchPath); file.open(QIODevice::WriteOnly); file.write(patchReply->readAll()); file.close(); - auto patch_path = patchesDir + "/" + name + ".patch"; - - patchQueue[ourIndex] = {name, repository, version, patch_path, hashes, hashBlockSize, length}; + patchQueue[ourIndex] = {name, repository, version, patchPath, hashes, hashBlockSize, length}; remainingPatches--; checkIfDone(); @@ -111,7 +113,7 @@ void Patcher::processPatchList(QNetworkAccessManager &mgr, const QString &patchL } else { qDebug() << "Found existing patch: " << name; - patchQueue[ourIndex] = {name, repository, version, patchesDir + "/" + name + ".patch", hashes, hashBlockSize, length}; + patchQueue[ourIndex] = {name, repository, version, patchPath, hashes, hashBlockSize, length}; remainingPatches--; checkIfDone(); @@ -190,3 +192,15 @@ void Patcher::processPatch(const QueuedPatch &patch) verFile.write(patch.version.toUtf8()); verFile.close(); } + +void Patcher::setupDirectories() +{ + QDir dataDir; + if (m_launcher.keepPatches()) { + dataDir.setPath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); + } else { + dataDir.setPath(QStandardPaths::writableLocation(QStandardPaths::TempLocation)); + } + + patchesDir.setPath(dataDir.absoluteFilePath("patches")); +} diff --git a/launcher/src/profile.cpp b/launcher/src/profile.cpp index fc1220b..70a2edd 100644 --- a/launcher/src/profile.cpp +++ b/launcher/src/profile.cpp @@ -498,3 +498,17 @@ QString Profile::wineVersionText() const return m_wineVersion; } } + +QString Profile::dalamudChannelName() const +{ + switch (dalamudChannel()) { + case DalamudChannel::Stable: + return QStringLiteral("stable"); + case DalamudChannel::Staging: + return QStringLiteral("staging"); + case DalamudChannel::Net5: + return QStringLiteral("net5"); + } + + Q_UNREACHABLE(); +} diff --git a/launcher/src/profilemanager.cpp b/launcher/src/profilemanager.cpp index 6bf1a9c..f5b6215 100644 --- a/launcher/src/profilemanager.cpp +++ b/launcher/src/profilemanager.cpp @@ -35,7 +35,6 @@ Profile *ProfileManager::addProfile() newProfile->readWineInfo(); - newProfile->setGamePath(getDefaultGamePath()); newProfile->setWinePrefixPath(getDefaultWinePrefixPath()); insertProfile(newProfile); @@ -68,7 +67,7 @@ QString ProfileManager::getDefaultWinePrefixPath() return ""; } -QString ProfileManager::getDefaultGamePath() +QString ProfileManager::getDefaultGamePath(const QString &uuid) { #if defined(Q_OS_WIN) return "C:\\Program Files (x86)\\SquareEnix\\FINAL FANTASY XIV - A Realm Reborn"; @@ -81,8 +80,9 @@ QString ProfileManager::getDefaultGamePath() #endif #if defined(Q_OS_LINUX) - const QString appData = QStandardPaths::standardLocations(QStandardPaths::StandardLocation::AppDataLocation)[0]; - return QStringLiteral("%1/game").arg(appData); + const QDir appData = QStandardPaths::standardLocations(QStandardPaths::StandardLocation::AppDataLocation)[0]; + const QDir gameDir = appData.absoluteFilePath("game"); + return gameDir.absoluteFilePath(uuid); #endif } diff --git a/launcher/src/utility.cpp b/launcher/src/utility.cpp new file mode 100644 index 0000000..3fbd644 --- /dev/null +++ b/launcher/src/utility.cpp @@ -0,0 +1,20 @@ +#include "utility.h" + +#include + +QDir Utility::stateDirectory() +{ + if (qEnvironmentVariableIsSet("XDG_STATE_HOME")) { + return qEnvironmentVariable("XDG_STATE_HOME"); + } + + const QDir homeDir = QStandardPaths::standardLocations(QStandardPaths::HomeLocation)[0]; + const QDir localDir = homeDir.absoluteFilePath(".local"); + const QDir stateDir = localDir.absoluteFilePath("state"); + return stateDir.absoluteFilePath("astra"); +} + +QString Utility::toWindowsPath(const QDir &dir) +{ + return "Z:" + dir.absolutePath().replace('/', '\\'); +} diff --git a/launcher/ui/Settings/DeveloperSettings.qml b/launcher/ui/Settings/DeveloperSettings.qml index 20ad127..59aa3d6 100644 --- a/launcher/ui/Settings/DeveloperSettings.qml +++ b/launcher/ui/Settings/DeveloperSettings.qml @@ -28,6 +28,8 @@ Kirigami.ScrollablePage { MobileForm.FormCheckDelegate { text: i18n("Keep Patches") description: i18n("Do not delete patches after they're used. Astra will not redownload patch data, if found.") + checked: LauncherCore.keepPatches + onCheckedChanged: LauncherCore.keepPatches = checked } } }