diff --git a/.clang-format b/.clang-format deleted file mode 100644 index aa0779e..0000000 --- a/.clang-format +++ /dev/null @@ -1,33 +0,0 @@ ---- -AllowShortIfStatementsOnASingleLine: Never -CompactNamespaces: 'false' -DisableFormat: 'false' -IndentCaseLabels: 'true' -IndentPPDirectives: BeforeHash -IndentWidth: '4' -Language: Cpp -NamespaceIndentation: All -PointerAlignment: Left -ReflowComments: 'true' -SortIncludes: 'true' -SortUsingDeclarations: 'true' -SpacesInCStyleCastParentheses: 'false' -Standard: Cpp11 -TabWidth: '0' -UseTab: Never -AllowShortEnumsOnASingleLine: false -BraceWrapping: - AfterEnum: true -AccessModifierOffset: -4 -SpaceAfterTemplateKeyword: 'false' -AllowAllParametersOfDeclarationOnNextLine: false -AlignAfterOpenBracket: AlwaysBreak -BinPackArguments: false -BinPackParameters: false -ColumnLimit: 120 -AllowShortBlocksOnASingleLine: 'false' -AllowShortCaseLabelsOnASingleLine: 'false' -AllowShortFunctionsOnASingleLine: 'Empty' -AllowShortLambdasOnASingleLine: 'Empty' -AllowShortLoopsOnASingleLine: 'false' -SeparateDefinitionBlocks : 'Always' diff --git a/.gitignore b/.gitignore index 4cbe1d5..a897565 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ .DS_Store .directory *.flatpak -export/ \ No newline at end of file +export/ +.clang-format \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index ed9d2c7..cdb64ef 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,30 +1,51 @@ cmake_minimum_required(VERSION 3.16) -project(Astra) +project(Astra VERSION 0.5.0 LANGUAGES CXX) # build options used for distributors option(BUILD_FLATPAK "Build for Flatpak." OFF) # options for features you may want or need -option(ENABLE_WATCHDOG "Build support for Watchdog, requires an X11 system." OFF) -option(ENABLE_STEAM "Build with Steam support, requires supplying the Steam SDK." OFF) +option(ENABLE_WATCHDOG "Build support for Watchdog, requires X11." OFF) +option(ENABLE_STEAM "Build with Steam support, requires supplying the Steam SDK yourself." OFF) option(ENABLE_GAMEMODE "Build with Feral GameMode support, requires the daemon to be installed." ON) -option(ENABLE_TABLET "Build support for the tablet interface, meant for devices like the Steam Deck." ON) -option(ENABLE_DESKTOP "Build support for the desktop interface, meant to be used on desktops and laptops." ON) -option(ENABLE_CLI "Build support for the command-line interface, meant for scripting and automation." ON) +set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) -find_package(Qt5 COMPONENTS Core Widgets Network Quick CONFIG REQUIRED) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(QT_MIN_VERSION 5.15) +set(KF5_MIN_VERSION 5.83) + +find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE) +list(APPEND CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) + +include(KDEInstallDirs) +include(ECMFindQmlModule) +include(KDECMakeSettings) +include(KDECompilerSettings NO_POLICY_SCOPE) +include(ECMSetupVersion) +include(ECMGenerateHeaders) +include(ECMPoQmTools) +include(KDEGitCommitHooks) +include(KDEClangFormat) + +find_package(Qt5 ${QT_MIN_VERSION} NO_MODULE REQUIRED COMPONENTS + Core + Widgets + Network + QuickControls2) +find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Kirigami2 I18n Config CoreAddons) +find_package(PkgConfig REQUIRED) if (ENABLE_WATCHDOG) - find_package(PkgConfig REQUIRED) pkg_search_module(TESSERACT REQUIRED tesseract) pkg_search_module(LEPTONICA REQUIRED lept) endif () if (ENABLE_GAMEMODE) - find_package(PkgConfig REQUIRED) pkg_search_module(GAMEMODE REQUIRED gamemode) endif () @@ -34,13 +55,20 @@ if (ENABLE_STEAM) INTERFACE_INCLUDE_DIRECTORIES ${STEAMWORKS_INCLUDE_DIR} IMPORTED_LOCATION ${STEAMWORKS_LIBRARIES}) - if(BUILD_FLATPAK) + if (BUILD_FLATPAK) install(IMPORTED_RUNTIME_ARTIFACTS Steamworks) - endif() + endif () endif () find_package(Qt5Keychain REQUIRED) find_package(QuaZip-Qt5 REQUIRED) add_subdirectory(external) -add_subdirectory(launcher) \ No newline at end of file +add_subdirectory(launcher) + +feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) + +file(GLOB_RECURSE ALL_CLANG_FORMAT_SOURCE_FILES src/*.cpp src/*.h) +kde_clang_format(${ALL_CLANG_FORMAT_SOURCE_FILES}) + +kde_configure_git_pre_commit_hook(CHECKS CLANG_FORMAT) \ No newline at end of file diff --git a/README.md b/README.md index 58b7478..09a93f2 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Astra A custom FFXIV launcher that supports multiple accounts, [Dalamud](https://github.com/goatcorp/Dalamud) plugins and runs -natively on Windows, macOS and Linux! +natively on Linux! ### Notice @@ -16,26 +16,22 @@ If you still have questions, please read the [FAQ](https://xiv.zone/astra/faq) f ## Features -* Traditional desktop interface which looks native to your system, utilizing Qt - a proven application framework. - * Supports single-window scenarios such as the Steam Deck seamlessly. -* Native support for Windows, macOS and Linux! -* Handles running Wine for macOS and Linux users - creating a seamless and native-feeling launcher experience, compared - to running other FFXIV launchers in Wine. +* Handles running Wine for you, creating a seamless and native-feeling launcher experience! * Can also easily enable several Linux-specific enhancements such as Fsync or configuring Gamescope. * Multiple account support! - * Most settings can be set per-profile. -* Easily install and use Dalamud plugins, just like XIVQuickLauncher. -* Patches the game, just like the official launcher! + * Can associate a Lodestone character with an account to use as an avatar. +* Easily install and use Dalamud plugins. +* Game patching support. * Securely login to the official Square Enix lobbies, as well as Sapphire servers. * Game arguments are encrypted by default, providing the same level of security as other launchers. * Saving account usernames and passwords are also supported, and is never stored plaintext. -* Can easily install FFXIV on new systems right from the launcher, bypassing the normal InstallShield installer. +* Can install FFXIV on new systems for you, bypassing the normal InstallShield installer. ## Installation -Precompiled binaries are available for Windows and macOS users, which you can [download from the website](https://xiv.zone/astra/install). +Precompiled binaries are available [to download from the website](https://xiv.zone/astra/install). -For Linux users, there is numerous options available to you: +There are also numerous options available: * _Flatpak_ - Instructions can be found in the [Flatpak installation](https://xiv.zone/astra/install/#linux) section. * _AUR_ - You can find the [AUR package here](https://aur.archlinux.org/packages/astra-launcher). @@ -52,11 +48,7 @@ This functionality will change in the future to ease distribution packaging. You the `USE_OWN_LIBRARIES` CMake option. [The wiki](https://man.sr.ht/~redstrate/astra/) has dedicated platform-specific pages for build instructions as well as -important information: - -* [Windows](https://man.sr.ht/~redstrate/astra/windows-usage.md) -* [macOS](https://man.sr.ht/~redstrate/astra/macos-usage.md) -* [Linux](https://man.sr.ht/~redstrate/astra/linux-usage.md) +[important usage information](https://man.sr.ht/~redstrate/astra/linux-usage.md). ## Contributing and Support diff --git a/compatibilitytool/toolmanifest.vdf b/compatibilitytool/toolmanifest.vdf index 991e02a..9704382 100644 --- a/compatibilitytool/toolmanifest.vdf +++ b/compatibilitytool/toolmanifest.vdf @@ -1,4 +1,5 @@ "manifest" { + "version" "2" "commandline" "/astra --steam %verb%" } diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index c29f0cb..06cb15d 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -1,3 +1,5 @@ +set(BUILD_SHARED_LIBS OFF) + add_subdirectory(libbaseencode) add_subdirectory(libcotp) @@ -6,9 +8,8 @@ include(FetchContent) FetchContent_Declare( Corrosion GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git - GIT_TAG v0.3.5 + GIT_TAG v0.4.2 ) - FetchContent_MakeAvailable(Corrosion) FetchContent_Declare( @@ -16,7 +17,6 @@ FetchContent_Declare( GIT_REPOSITORY https://git.sr.ht/~redstrate/libphysis GIT_TAG main ) - FetchContent_MakeAvailable(libphysis) corrosion_import_crate(MANIFEST_PATH ${libphysis_SOURCE_DIR}/Cargo.toml diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 2263d82..256e17d 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -1,29 +1,88 @@ -add_subdirectory(core) -add_subdirectory(desktop) +add_executable(astra) +target_sources(astra PRIVATE + include/account.h + include/accountmanager.h + include/assetupdater.h + include/encryptedarg.h + include/gameinstaller.h + include/headline.h + include/launchercore.h + include/patcher.h + include/profile.h + include/profilemanager.h + include/sapphirelauncher.h + include/squareboot.h + include/squarelauncher.h + include/steamapi.h -add_executable(astra - main.cpp) + src/account.cpp + src/accountmanager.cpp + src/assetupdater.cpp + src/encryptedarg.cpp + src/gameinstaller.cpp + src/launchercore.cpp + src/main.cpp + src/patcher.cpp + src/profile.cpp + src/profilemanager.cpp + src/sapphirelauncher.cpp + src/squareboot.cpp + src/squarelauncher.cpp + src/steamapi.cpp -target_link_libraries(astra PUBLIC - astra_core - astra_desktop) -target_compile_features(astra PUBLIC cxx_std_17) -set_target_properties(astra PROPERTIES CXX_EXTENSIONS OFF) + resources.qrc) +kconfig_add_kcfg_files(astra GENERATE_MOC config.kcfgc accountconfig.kcfgc profileconfig.kcfgc) +target_include_directories(astra PRIVATE include) +target_link_libraries(astra PRIVATE + physis + cotp + crypto + QuaZip::QuaZip + Qt5Keychain::Qt5Keychain + Qt5::Core + Qt5::Network + Qt5::Widgets + Qt5::Quick + Qt5::QuickControls2 + KF5::Kirigami2 + KF5::I18n + KF5::ConfigCore + KF5::ConfigGui + KF5::CoreAddons) -# meant for including the license text -file(READ ${CMAKE_CURRENT_SOURCE_DIR}/../LICENSE LICENSE_TXT) -STRING(REPLACE "\n" " \\n" LICENSE_TXT ${LICENSE_TXT}) -STRING(REPLACE "\"" "\"\"" LICENSE_TXT ${LICENSE_TXT}) +if (ENABLE_WATCHDOG) + target_sources(astra PRIVATE + include/gameparser.h + include/watchdog.h -configure_file(${CMAKE_CURRENT_LIST_DIR}/../cmake/license.h.in - ${CMAKE_BINARY_DIR}/license.h) + src/gameparser.cpp + src/watchdog.cpp) + target_link_libraries(astra PRIVATE + ${TESSERACT_LIBRARIES} + ${LEPTONICA_LIBRARIES} + X11 + Xcomposite + Xrender) + + target_include_directories(astra_core PRIVATE ${TESSERACT_INCLUDE_DIRS} ${LEPTONICA_INCLUDE_DIRS}) + target_compile_definitions(astra_core PRIVATE ENABLE_WATCHDOG) +endif () if (BUILD_FLATPAK) target_compile_definitions(astra PRIVATE FLATPAK) endif () -install(TARGETS astra - DESTINATION "${INSTALL_BIN_PATH}") +if (ENABLE_STEAM) + target_link_libraries(astra PRIVATE Steamworks) + target_compile_definitions(astra PRIVATE ENABLE_STEAM) +endif () + +if (ENABLE_GAMEMODE) + target_link_libraries(astra PRIVATE ${GAMEMODE_LIBRARIES}) + target_compile_definitions(astra PRIVATE ENABLE_GAMEMODE) +endif () + +install(TARGETS astra ${KF${QT_MAJOR_VERSION}_INSTALL_TARGETS_DEFAULT_ARGS}) if (WIN32) get_target_property(QMAKE_EXE Qt5::qmake IMPORTED_LOCATION) diff --git a/launcher/accountconfig.kcfg b/launcher/accountconfig.kcfg new file mode 100644 index 0000000..d17259e --- /dev/null +++ b/launcher/accountconfig.kcfg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + false + + + + + false + + + false + + + false + + + + + + + + + + + WindowsStandalone + + + + + diff --git a/launcher/accountconfig.kcfgc b/launcher/accountconfig.kcfgc new file mode 100644 index 0000000..3c432f9 --- /dev/null +++ b/launcher/accountconfig.kcfgc @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2023 Joshua Goins +# SPDX-License-Identifier: LGPL-2.1-or-later +File=accountconfig.kcfg +ClassName=AccountConfig +Mutators=true +DefaultValueGetters=true +GenerateProperties=true +ParentInConstructor=true +Singleton=false diff --git a/launcher/config.kcfg b/launcher/config.kcfg new file mode 100644 index 0000000..1483770 --- /dev/null +++ b/launcher/config.kcfg @@ -0,0 +1,24 @@ + + + + + + + true + + + true + + + true + + + + + diff --git a/launcher/config.kcfgc b/launcher/config.kcfgc new file mode 100644 index 0000000..972b99a --- /dev/null +++ b/launcher/config.kcfgc @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2023 Joshua Goins +# SPDX-License-Identifier: LGPL-2.1-or-later +File=config.kcfg +ClassName=Config +Mutators=true +DefaultValueGetters=true +GenerateProperties=true +ParentInConstructor=true +Singleton=true diff --git a/launcher/core/CMakeLists.txt b/launcher/core/CMakeLists.txt deleted file mode 100644 index af4170e..0000000 --- a/launcher/core/CMakeLists.txt +++ /dev/null @@ -1,85 +0,0 @@ -set(HEADERS - include/config.h - include/encryptedarg.h - include/gameinstaller.h - include/headline.h - include/launchercore.h - include/sapphirelauncher.h - include/squareboot.h - include/squarelauncher.h - include/patcher.h - include/steamapi.h - include/assetupdater.h) - -set(SRC - src/encryptedarg.cpp - src/gameinstaller.cpp - src/headline.cpp - src/launchercore.cpp - src/sapphirelauncher.cpp - src/squareboot.cpp - src/squarelauncher.cpp - src/patcher.cpp - src/steamapi.cpp - src/assetupdater.cpp) - -if (ENABLE_WATCHDOG) - set(HEADERS ${HEADERS} - include/gameparser.h - include/watchdog.h) - - set(SRC ${SRC} - src/gameparser.cpp - src/watchdog.cpp) - - set(LIBRARIES ${LIBRARIES} - ${TESSERACT_LIBRARIES} - ${LEPTONICA_LIBRARIES} - X11 - Xcomposite - Xrender) -endif () - -if(ENABLE_STEAM) - set(LIBRARIES ${LIBRARIES} - Steamworks) -endif() - -if(ENABLE_GAMEMODE) - set(LIBRARIES ${LIBRARIES} - ${GAMEMODE_LIBRARIES}) -endif() - -add_library(astra_core STATIC ${HEADERS} ${SRC}) -target_include_directories(astra_core PUBLIC - ${KEYCHAIN_INCLUDE_DIRS} - ${QUAZIP_INCLUDE_DIRS} - ${CMAKE_BINARY_DIR} - include) -target_link_libraries(astra_core PUBLIC - physis - z # FIXME: remove! - QuaZip::QuaZip - Qt5Keychain::Qt5Keychain - ${LIBRARIES} - Qt5::Core - Qt5::Network - Qt5::Widgets # widgets is required by watchdog, to be fixed/removed later - Qt5::Quick # required for some type registrations - PRIVATE - cotp - crypto) # desktop is currently required by the core, to be fixed/removed later - -if (ENABLE_WATCHDOG) - target_include_directories(astra_core PUBLIC ${TESSERACT_INCLUDE_DIRS} ${LEPTONICA_INCLUDE_DIRS}) - - target_compile_definitions(astra_core PUBLIC ENABLE_WATCHDOG) -endif () - -if(ENABLE_GAMEMODE) - target_compile_definitions(astra_core PUBLIC ENABLE_GAMEMODE) -endif() - -if(ENABLE_STEAM) - target_compile_definitions(astra_core PUBLIC ENABLE_STEAM) -endif() \ No newline at end of file diff --git a/launcher/core/include/config.h b/launcher/core/include/config.h deleted file mode 100644 index 802988d..0000000 --- a/launcher/core/include/config.h +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -constexpr const char* version = "0.4.1"; \ No newline at end of file diff --git a/launcher/core/include/encryptedarg.h b/launcher/core/include/encryptedarg.h deleted file mode 100644 index f521cab..0000000 --- a/launcher/core/include/encryptedarg.h +++ /dev/null @@ -1,42 +0,0 @@ -#pragma once - -#include -#include - -// from xivdev -static char ChecksumTable[] = {'f', 'X', '1', 'p', 'G', 't', 'd', 'S', '5', 'C', 'A', 'P', '4', '_', 'V', 'L'}; - -inline char GetChecksum(const unsigned int key) { - auto value = key & 0x000F0000; - return ChecksumTable[value >> 16]; -} - -uint32_t TickCount(); - -inline QString encryptGameArg(const QString& arg) { - const uint32_t rawTicks = TickCount(); - const uint32_t ticks = rawTicks & 0xFFFFFFFFu; - const uint32_t key = ticks & 0xFFFF0000u; - - char buffer[9]{}; - sprintf(buffer, "%08x", key); - - Blowfish const* blowfish = physis_blowfish_initialize(reinterpret_cast(buffer), 9); - - uint8_t* out_data = nullptr; - uint32_t out_size = 0; - - QByteArray toEncrypt = (QString(" /T =%1").arg(ticks) + arg).toUtf8(); - - physis_blowfish_encrypt( - blowfish, reinterpret_cast(toEncrypt.data()), toEncrypt.size(), &out_data, &out_size); - - const QByteArray encryptedArg = - QByteArray::fromRawData(reinterpret_cast(out_data), static_cast(out_size)); - - const QString base64 = encryptedArg.toBase64( - QByteArray::Base64Option::Base64UrlEncoding | QByteArray::Base64Option::KeepTrailingEquals); - const char checksum = GetChecksum(key); - - return QString("//**sqex0003%1%2**//").arg(base64, QString(checksum)); -} \ No newline at end of file diff --git a/launcher/core/include/gameinstaller.h b/launcher/core/include/gameinstaller.h deleted file mode 100644 index 5ab997d..0000000 --- a/launcher/core/include/gameinstaller.h +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -#include -#include - -class LauncherCore; -class ProfileSettings; - -// TODO: convert to a nice signal/slots class like assetupdater -void installGame(LauncherCore& launcher, ProfileSettings& profile, const std::function& returnFunc); \ No newline at end of file diff --git a/launcher/core/include/headline.h b/launcher/core/include/headline.h deleted file mode 100644 index b8bbec5..0000000 --- a/launcher/core/include/headline.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include -#include - -struct News { - QDateTime date; - QString id; - QString tag; - QString title; - QUrl url; -}; - -struct Banner { - QUrl link; - QUrl bannerImage; -}; - -struct Headline { - QList banner; - - QList news; - - QList pinned; - - QList topics; -}; - -class LauncherCore; - -void getHeadline(LauncherCore& core, const std::function& return_func); \ No newline at end of file diff --git a/launcher/core/include/launchercore.h b/launcher/core/include/launchercore.h deleted file mode 100755 index 5d78f6a..0000000 --- a/launcher/core/include/launchercore.h +++ /dev/null @@ -1,266 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "squareboot.h" -#include "steamapi.h" - -class SapphireLauncher; -class SquareLauncher; -class AssetUpdater; -class Watchdog; - -enum class GameLicense { - WindowsStandalone, - WindowsSteam, - macOS -}; - -enum class WineType { - System, - Custom, - Builtin, // macos only - XIVOnMac // macos only -}; - -enum class DalamudChannel { - Stable, - Staging, - Net5 -}; - -class ProfileSettings : public QObject { - Q_OBJECT - QML_ELEMENT -public: - QUuid uuid; - QString name; - - // game - int language = 1; // 1 is english, thats all i know - QString gamePath, winePath, winePrefixPath; - QString wineVersion; - bool enableWatchdog = false; - - BootData* bootData; - GameData* gameData; - - physis_Repositories repositories; - const char* bootVersion; - - [[nodiscard]] bool isGameInstalled() const { - return repositories.repositories_count > 0; - } - - [[nodiscard]] bool isWineInstalled() const { - return !wineVersion.isEmpty(); - } - -#if defined(Q_OS_MAC) - WineType wineType = WineType::Builtin; -#else - WineType wineType = WineType::System; -#endif - - bool useEsync = false, useGamescope = false, useGamemode = false; - bool useDX9 = false; - bool enableDXVKhud = false; - - struct GamescopeOptions { - bool fullscreen = true; - bool borderless = true; - int width = 0; - int height = 0; - int refreshRate = 0; - } gamescope; - - struct DalamudOptions { - bool enabled = false; - bool optOutOfMbCollection = false; - DalamudChannel channel = DalamudChannel::Stable; - } dalamud; - - // login - bool encryptArguments = true; - bool isSapphire = false; - QString lobbyURL; - bool rememberUsername = false, rememberPassword = false; - bool rememberOTPSecret = false; - bool useOneTimePassword = false; - bool autoLogin = false; - - GameLicense license = GameLicense::WindowsStandalone; - bool isFreeTrial = false; - - /* - * Sets a value in the keychain. This function is asynchronous. - */ - void setKeychainValue(const QString& key, const QString& value) const; - - /* - * Retrieves a value from the keychain. This function is synchronous. - */ - QString getKeychainValue(const QString& key) const; -}; - -struct AppSettings { - bool closeWhenLaunched = true; - bool showBanners = true; - bool showNewsList = true; -}; - -class LoginInformation : public QObject { - Q_OBJECT - Q_PROPERTY(QString username MEMBER username) - Q_PROPERTY(QString password MEMBER password) - Q_PROPERTY(QString oneTimePassword MEMBER oneTimePassword) - Q_PROPERTY(ProfileSettings* settings MEMBER settings) - QML_ELEMENT -public: - ProfileSettings* settings = nullptr; - - QString username, password, oneTimePassword; -}; - -struct LoginAuth { - QString SID; - int region = 2; // america? - int maxExpansion = 1; - - // if empty, dont set on the client - QString lobbyhost, frontierHost; -}; - -class LauncherCore : public QObject { - Q_OBJECT - Q_PROPERTY(SquareBoot* squareBoot MEMBER squareBoot) -public: - explicit LauncherCore(bool isSteam); - - // used for qml only, TODO: move this to a dedicated factory - Q_INVOKABLE LoginInformation* createNewLoginInfo() { - return new LoginInformation(); - } - - QNetworkAccessManager* mgr; - - ProfileSettings& getProfile(int index); - - // used for qml only - Q_INVOKABLE ProfileSettings* getProfileQML(int index) { - return profileSettings[index]; - } - - int getProfileIndex(const QString& name); - Q_INVOKABLE [[nodiscard]] QList profileList() const; - int addProfile(); - int deleteProfile(const QString& name); - - /* - * Begins the login process, and may call SquareBoot or SapphireLauncher depending on the profile type. - * It's designed to be opaque as possible to the caller. - * - * The login process is asynchronous. - */ - Q_INVOKABLE void login(LoginInformation* loginInformation); - - /* - * Attempts to log into a profile without LoginInformation, which may or may not work depending on a combination of - * the password failing, OTP not being available to auto-generate, among other things. - * - * The launcher will still warn the user about any possible errors, however the call site will need to check the - * result to see whether they need to "reset" or show a failed state or not. - */ - bool autoLogin(ProfileSettings& settings); - - /* - * Launches the game using the provided authentication. - */ - void launchGame(const ProfileSettings& settings, const LoginAuth& auth); - - /* - * This just wraps it in wine if needed. - */ - void launchExecutable( - const ProfileSettings& settings, - QProcess* process, - const QStringList& args, - bool isGame, - bool needsRegistrySetup); - - void addRegistryKey(const ProfileSettings& settings, QString key, QString value, QString data); - - void buildRequest(const ProfileSettings& settings, QNetworkRequest& request); - void setSSL(QNetworkRequest& request); - void readInitialInformation(); - void readGameVersion(); - void readWineInfo(ProfileSettings& settings); - void saveSettings(); - - QSettings settings; - - SapphireLauncher* sapphireLauncher; - SquareBoot* squareBoot; - SquareLauncher* squareLauncher; - AssetUpdater* assetUpdater; - Watchdog* watchdog; - - bool gamescopeAvailable = false; - bool gamemodeAvailable = false; - - AppSettings appSettings; - - QString dalamudVersion; - int dalamudAssetVersion = -1; - QString runtimeVersion; - - int defaultProfileIndex = 0; - - QVector expansionNames; - - bool isSteam = false; - -signals: - void settingsChanged(); - void successfulLaunch(); - void gameClosed(); - -private: - /* - * Begins the game executable, but calls to Dalamud if needed. - */ - void beginGameExecutable(const ProfileSettings& settings, const LoginAuth& auth); - - /* - * Starts a vanilla game session with no Dalamud injection. - */ - void beginVanillaGame(const QString& gameExecutablePath, const ProfileSettings& profile, const LoginAuth& auth); - - /* - * Starts a game session with Dalamud injected. - */ - void beginDalamudGame(const QString& gameExecutablePath, const ProfileSettings& profile, const LoginAuth& auth); - - /* - * Returns the game arguments needed to properly launch the game. This encrypts it too if needed, and it's already - * joined! - */ - QString getGameArgs(const ProfileSettings& profile, const LoginAuth& auth); - - bool checkIfInPath(const QString& program); - void readGameData(ProfileSettings& profile); - - QString getDefaultGamePath(); - QString getDefaultWinePrefixPath(); - - QVector profileSettings; - - SteamAPI* steamApi = nullptr; -}; diff --git a/launcher/core/include/sapphirelauncher.h b/launcher/core/include/sapphirelauncher.h deleted file mode 100644 index 76345c6..0000000 --- a/launcher/core/include/sapphirelauncher.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include - -#include "launchercore.h" - -class SapphireLauncher : QObject { -public: - explicit SapphireLauncher(LauncherCore& window); - - void login(const QString& lobbyUrl, const LoginInformation& info); - void registerAccount(const QString& lobbyUrl, const LoginInformation& info); - -private: - LauncherCore& window; -}; \ No newline at end of file diff --git a/launcher/core/include/squareboot.h b/launcher/core/include/squareboot.h deleted file mode 100644 index 11feb0b..0000000 --- a/launcher/core/include/squareboot.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include "patcher.h" - -class SquareLauncher; -class LauncherCore; -struct LoginInformation; - -class SquareBoot : public QObject { - Q_OBJECT -public: - SquareBoot(LauncherCore& window, SquareLauncher& launcher); - - Q_INVOKABLE void checkGateStatus(LoginInformation* info); - - void bootCheck(const LoginInformation& info); - -private: - Patcher* patcher = nullptr; - - LauncherCore& window; - SquareLauncher& launcher; -}; \ No newline at end of file diff --git a/launcher/core/include/squarelauncher.h b/launcher/core/include/squarelauncher.h deleted file mode 100644 index 50e2b95..0000000 --- a/launcher/core/include/squarelauncher.h +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include "launchercore.h" -#include "patcher.h" - -class SquareLauncher : public QObject { - Q_OBJECT -public: - explicit SquareLauncher(LauncherCore& window); - - void getStored(const LoginInformation& info); - - void login(const LoginInformation& info, const QUrl& referer); - - void registerSession(const LoginInformation& info); - -private: - QString getBootHash(const LoginInformation& info); - - Patcher* patcher = nullptr; - - QString stored, SID, username; - LoginAuth auth; - - LauncherCore& window; -}; diff --git a/launcher/core/include/steamapi.h b/launcher/core/include/steamapi.h deleted file mode 100644 index efaac31..0000000 --- a/launcher/core/include/steamapi.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -class LauncherCore; - -class SteamAPI { -public: - explicit SteamAPI(LauncherCore& core); - - void setLauncherMode(bool isLauncher); - - [[nodiscard]] bool isDeck() const; - -private: - LauncherCore& core; -}; \ No newline at end of file diff --git a/launcher/core/src/encryptedarg.cpp b/launcher/core/src/encryptedarg.cpp deleted file mode 100644 index 55dbff4..0000000 --- a/launcher/core/src/encryptedarg.cpp +++ /dev/null @@ -1,40 +0,0 @@ -#include "encryptedarg.h" - -#if defined(Q_OS_MAC) - #include - #include -#endif - -#if defined(Q_OS_WIN) - #include -#endif - -#if defined(Q_OS_MAC) -// taken from XIV-on-Mac, apparently Wine changed this? -uint32_t TickCount() { - struct mach_timebase_info timebase; - mach_timebase_info(&timebase); - - auto machtime = mach_continuous_time(); - auto numer = uint64_t(timebase.numer); - auto denom = uint64_t(timebase.denom); - auto monotonic_time = machtime * numer / denom / 100; - return monotonic_time / 10000; -} -#endif - -#if defined(Q_OS_LINUX) -uint32_t TickCount() { - struct timespec ts{}; - - clock_gettime(CLOCK_MONOTONIC, &ts); - - return (ts.tv_sec * 1000 + ts.tv_nsec / 1000000); -} -#endif - -#if defined(Q_OS_WIN) -uint32_t TickCount() { - return GetTickCount(); -} -#endif \ No newline at end of file diff --git a/launcher/core/src/headline.cpp b/launcher/core/src/headline.cpp deleted file mode 100644 index d8ee222..0000000 --- a/launcher/core/src/headline.cpp +++ /dev/null @@ -1,84 +0,0 @@ -#include "headline.h" - -#include -#include -#include -#include -#include -#include - -#include "launchercore.h" - -void getHeadline(LauncherCore& core, const std::function& return_func) { - QUrlQuery query; - query.addQueryItem("lang", "en-us"); - query.addQueryItem("media", "pcapp"); - - QUrl url; - url.setScheme("https"); - url.setHost("frontier.ffxiv.com"); - url.setPath("/news/headline.json"); - url.setQuery(query); - - auto request = - QNetworkRequest(QString("%1&%2").arg(url.toString(), QString::number(QDateTime::currentMSecsSinceEpoch()))); - - // TODO: really? - core.buildRequest(core.getProfile(core.defaultProfileIndex), request); - - request.setRawHeader("Accept", "application/json, text/plain, */*"); - request.setRawHeader("Origin", "https://launcher.finalfantasyxiv.com"); - request.setRawHeader( - "Referer", - QString("https://launcher.finalfantasyxiv.com/v600/index.html?rc_lang=%1&time=%2") - .arg("en-us", QDateTime::currentDateTimeUtc().toString("yyyy-MM-dd-HH")) - .toUtf8()); - - auto reply = core.mgr->get(request); - QObject::connect(reply, &QNetworkReply::finished, [=] { - auto document = QJsonDocument::fromJson(reply->readAll()); - - Headline headline; - - const auto parseNews = [](QJsonObject object) -> News { - News news; - news.date = QDateTime::fromString(object["date"].toString(), Qt::DateFormat::ISODate); - news.id = object["id"].toString(); - news.tag = object["tag"].toString(); - news.title = object["title"].toString(); - - if (object["url"].toString().isEmpty()) { - news.url = QUrl(QString("https://na.finalfantasyxiv.com/lodestone/news/detail/%1").arg(news.id)); - } else { - news.url = QUrl(object["url"].toString()); - } - - return news; - }; - - for (auto bannerObject : document.object()["banner"].toArray()) { - Banner banner; - banner.link = QUrl(bannerObject.toObject()["link"].toString()); - banner.bannerImage = QUrl(bannerObject.toObject()["lsb_banner"].toString()); - - headline.banner.push_back(banner); - } - - for (auto newsObject : document.object()["news"].toArray()) { - auto news = parseNews(newsObject.toObject()); - headline.news.push_back(news); - } - - for (auto pinnedObject : document.object()["pinned"].toArray()) { - auto pinned = parseNews(pinnedObject.toObject()); - headline.pinned.push_back(pinned); - } - - for (auto pinnedObject : document.object()["topics"].toArray()) { - auto pinned = parseNews(pinnedObject.toObject()); - headline.topics.push_back(pinned); - } - - return_func(headline); - }); -} \ No newline at end of file diff --git a/launcher/core/src/launchercore.cpp b/launcher/core/src/launchercore.cpp deleted file mode 100755 index 86de0a4..0000000 --- a/launcher/core/src/launchercore.cpp +++ /dev/null @@ -1,725 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef ENABLE_GAMEMODE -#include -#endif - -#include "assetupdater.h" -#include "encryptedarg.h" -#include "launchercore.h" -#include "sapphirelauncher.h" -#include "squareboot.h" -#include "squarelauncher.h" - -#ifdef ENABLE_WATCHDOG - #include "watchdog.h" -#endif - -void LauncherCore::setSSL(QNetworkRequest& request) { - QSslConfiguration config; - config.setProtocol(QSsl::AnyProtocol); - config.setPeerVerifyMode(QSslSocket::VerifyNone); - - request.setSslConfiguration(config); -} - -void LauncherCore::buildRequest(const ProfileSettings& settings, QNetworkRequest& request) { - setSSL(request); - - if (settings.license == GameLicense::macOS) { - request.setHeader(QNetworkRequest::UserAgentHeader, "macSQEXAuthor/2.0.0(MacOSX; ja-jp)"); - } else { - request.setHeader( - QNetworkRequest::UserAgentHeader, - QString("SQEXAuthor/2.0.0(Windows 6.2; ja-jp; %1)").arg(QString(QSysInfo::bootUniqueId()))); - } - - request.setRawHeader( - "Accept", - "image/gif, image/jpeg, image/pjpeg, application/x-ms-application, application/xaml+xml, " - "application/x-ms-xbap, */*"); - request.setRawHeader("Accept-Encoding", "gzip, deflate"); - request.setRawHeader("Accept-Language", "en-us"); -} - -void LauncherCore::launchGame(const ProfileSettings& profile, const LoginAuth& auth) { - steamApi->setLauncherMode(false); - -#ifdef ENABLE_WATCHDOG - if (profile.enableWatchdog) { - watchdog->launchGame(profile, auth); - } else { - beginGameExecutable(profile, auth); - } -#else - beginGameExecutable(profile, auth); -#endif -} - -void LauncherCore::beginGameExecutable(const ProfileSettings& profile, const LoginAuth& auth) { - QString gameExectuable; - if(profile.useDX9) { - gameExectuable = profile.gamePath + "/game/ffxiv.exe"; - } else { - gameExectuable = profile.gamePath + "/game/ffxiv_dx11.exe"; - } - - if(profile.dalamud.enabled) { - beginDalamudGame(gameExectuable, profile, auth); - } else { - beginVanillaGame(gameExectuable, profile, auth); - } - - successfulLaunch(); -} - -void LauncherCore::beginVanillaGame(const QString& gameExecutablePath, const ProfileSettings& profile, const LoginAuth& auth) { - auto gameProcess = new QProcess(); - gameProcess->setProcessEnvironment(QProcessEnvironment::systemEnvironment()); - - auto args = getGameArgs(profile, auth); - - launchExecutable( - profile, - gameProcess, - {gameExecutablePath, args}, - true, - true); -} - -void LauncherCore::beginDalamudGame(const QString& gameExecutablePath, const ProfileSettings& profile, const LoginAuth& auth) { - QString gamePath = gameExecutablePath; - gamePath = "Z:" + gamePath.replace('/', '\\'); - - QString dataDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); - dataDir = "Z:" + dataDir.replace('/', '\\'); - - auto dalamudProcess = new QProcess(); - - QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); - env.insert("DALAMUD_RUNTIME", dataDir + "\\DalamudRuntime"); - -#if defined(Q_OS_LINUX) || defined(Q_OS_MAC) - env.insert("XL_WINEONLINUX", "true"); -#endif - dalamudProcess->setProcessEnvironment(env); - - auto args = getGameArgs(profile, auth); - - launchExecutable( - profile, - dalamudProcess, - {dataDir + "/Dalamud/" + "Dalamud.Injector.exe", "launch", "-m", "inject", - "--game=" + gamePath, - "--dalamud-configuration-path=" + dataDir + "\\dalamudConfig.json", - "--dalamud-plugin-directory=" + dataDir + "\\installedPlugins", - "--dalamud-asset-directory=" + dataDir + "\\DalamudAssets", - "--dalamud-client-language=" + QString::number(profile.language), - "--", - args}, - true, - true); -} - -QString LauncherCore::getGameArgs(const ProfileSettings& profile, const LoginAuth& auth) { - struct Argument { - QString key, value; - }; - - QList gameArgs; - gameArgs.push_back({"DEV.DataPathType", QString::number(1)}); - gameArgs.push_back({"DEV.UseSqPack", QString::number(1)}); - - gameArgs.push_back({"DEV.MaxEntitledExpansionID", QString::number(auth.maxExpansion)}); - gameArgs.push_back({"DEV.TestSID", auth.SID}); - 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}); - - if (!auth.lobbyhost.isEmpty()) { - gameArgs.push_back({"DEV.GMServerHost", auth.frontierHost}); - for (int i = 1; i < 9; i++) { - gameArgs.push_back({QString("DEV.LobbyHost0%1").arg(QString::number(i)), auth.lobbyhost}); - gameArgs.push_back({QString("DEV.LobbyPort0%1").arg(QString::number(i)), QString::number(54994)}); - } - } - - if (profile.license == GameLicense::WindowsSteam) { - gameArgs.push_back({"IsSteam", "1"}); - } - - const QString argFormat = profile.encryptArguments ? " /%1 =%2" : " %1=%2"; - - QString argJoined; - for (const auto& arg : gameArgs) { - argJoined += argFormat.arg(arg.key, arg.value); - } - - return profile.encryptArguments ? encryptGameArg(argJoined) : argJoined; -} - -void LauncherCore::launchExecutable( - const ProfileSettings& profile, - QProcess* process, - const QStringList& args, - bool isGame, - bool needsRegistrySetup) { - QList arguments; - auto env = process->processEnvironment(); - - if (needsRegistrySetup) { -#if defined(Q_OS_LINUX) || defined(Q_OS_MAC) - if (profile.license == GameLicense::macOS) { - addRegistryKey(profile, "HKEY_CURRENT_USER\\Software\\Wine", "HideWineExports", "0"); - } else { - addRegistryKey(profile, "HKEY_CURRENT_USER\\Software\\Wine", "HideWineExports", "1"); - } -#endif - } - -#if defined(Q_OS_LINUX) - if (isGame) { - if (profile.useGamescope) { - arguments.push_back("gamescope"); - - if (profile.gamescope.fullscreen) - arguments.push_back("-f"); - - if (profile.gamescope.borderless) - arguments.push_back("-b"); - - if (profile.gamescope.width > 0) - arguments.push_back("-w " + QString::number(profile.gamescope.width)); - - if (profile.gamescope.height > 0) - arguments.push_back("-h " + QString::number(profile.gamescope.height)); - - if (profile.gamescope.refreshRate > 0) - arguments.push_back("-r " + QString::number(profile.gamescope.refreshRate)); - } - } -#endif - -#if ENABLE_GAMEMODE - if(isGame && profile.useGamemode) { - gamemode_request_start(); - } -#endif - -#if defined(Q_OS_LINUX) - if (profile.useEsync) { - env.insert("WINEESYNC", QString::number(1)); - env.insert("WINEFSYNC", QString::number(1)); - env.insert("WINEFSYNC_FUTEX2", QString::number(1)); - } -#endif - -#if defined(Q_OS_MAC) || defined(Q_OS_LINUX) - if(isSteam) { - const QString steamDirectory = QProcessEnvironment::systemEnvironment().value("STEAM_COMPAT_CLIENT_INSTALL_PATH"); - const QString compatData = steamDirectory + "/steamapps/compatdata/39210"; // TODO: do these have to exist on the root steam folder? - const QString protonPath = steamDirectory + "/steamapps/common/Proton 7.0"; - - env.insert("PATH", protonPath + "/dist/bin:" + QProcessEnvironment::systemEnvironment().value("PATH")); - env.insert("WINEDLLPATH", protonPath + "/dist/lib64/wine:" + protonPath + "/dist/lib/wine"); - env.insert("LD_LIBRARY_PATH", protonPath + "/dist/lib64:" + protonPath + "/dist/lib"); - env.insert("WINEPREFIX", compatData + "/pfx"); - env.insert("STEAM_COMPAT_CLIENT_INSTALL_PATH", steamDirectory); - env.insert("STEAM_COMPAT_DATA_PATH", compatData); - - arguments.push_back(protonPath + "/proton"); - arguments.push_back("run"); - } else { - env.insert("WINEPREFIX", profile.winePrefixPath); - - // XIV on Mac bundle their own Wine install directory, complete with libs etc - if (profile.wineType == WineType::XIVOnMac) { - // TODO: don't hardcode this - QString xivLibPath = "/Applications/XIV on Mac.app/Contents/Resources/wine/lib:/Applications/XIV on " - "Mac.app/Contents/Resources/MoltenVK/modern"; - - env.insert("DYLD_FALLBACK_LIBRARY_PATH", xivLibPath); - env.insert("DYLD_VERSIONED_LIBRARY_PATH", xivLibPath); - env.insert("MVK_CONFIG_FULL_IMAGE_VIEW_SWIZZLE", "1"); - env.insert("MVK_CONFIG_RESUME_LOST_DEVICE", "1"); - env.insert("MVK_ALLOW_METAL_FENCES", "1"); - env.insert("MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS", "1"); - } - - #if defined(FLATPAK) - arguments.push_back("flatpak-spawn"); - arguments.push_back("--host"); - #endif - arguments.push_back(profile.winePath); - } -#endif - - arguments.append(args); - - auto executable = arguments[0]; - arguments.removeFirst(); - - if (isGame) - process->setWorkingDirectory(profile.gamePath + "/game/"); - - process->setProcessEnvironment(env); - - process->start(executable, arguments); -} - -void LauncherCore::readInitialInformation() { - defaultProfileIndex = settings.value("defaultProfile", 0).toInt(); - - auto defaultAppSettings = AppSettings(); - appSettings.closeWhenLaunched = settings.value("closeWhenLaunched", defaultAppSettings.closeWhenLaunched).toBool(); - appSettings.showBanners = settings.value("showBanners", defaultAppSettings.showBanners).toBool(); - appSettings.showNewsList = settings.value("showNewsList", defaultAppSettings.showNewsList).toBool(); - - gamescopeAvailable = checkIfInPath("gamescope"); - gamemodeAvailable = checkIfInPath("gamemoderun"); - - const QString dataDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); - - const bool hasDalamud = QFile::exists(dataDir + "/Dalamud"); - if (hasDalamud) { - if (QFile::exists(dataDir + "/Dalamud/Dalamud.deps.json")) { - QFile depsJson(dataDir + "/Dalamud/Dalamud.deps.json"); - depsJson.open(QFile::ReadOnly); - QJsonDocument doc = QJsonDocument::fromJson(depsJson.readAll()); - - QString versionString; - if (doc["targets"].toObject().contains(".NETCoreApp,Version=v5.0")) { - versionString = - doc["targets"].toObject()[".NETCoreApp,Version=v5.0"].toObject().keys().filter("Dalamud")[0]; - } else { - versionString = - doc["targets"].toObject()[".NETCoreApp,Version=v6.0"].toObject().keys().filter("Dalamud")[0]; - } - - dalamudVersion = versionString.remove("Dalamud/"); - } - - if (QFile::exists(dataDir + "/DalamudAssets/asset.ver")) { - QFile assetJson(dataDir + "/DalamudAssets/asset.ver"); - assetJson.open(QFile::ReadOnly | QFile::Text); - - dalamudAssetVersion = QString(assetJson.readAll()).toInt(); - } - - if (QFile::exists(dataDir + "/DalamudRuntime/runtime.ver")) { - QFile runtimeVer(dataDir + "/DalamudRuntime/runtime.ver"); - runtimeVer.open(QFile::ReadOnly | QFile::Text); - - runtimeVersion = QString(runtimeVer.readAll()); - } - } - - auto profiles = settings.childGroups(); - - // create the Default profile if it doesnt exist - if (profiles.empty()) - profiles.append(QUuid::createUuid().toString(QUuid::StringFormat::WithoutBraces)); - - profileSettings.resize(profiles.size()); - - for (const auto& uuid : profiles) { - auto profile = new ProfileSettings(); - profile->uuid = QUuid(uuid); - - settings.beginGroup(uuid); - - profile->name = settings.value("name", "Default").toString(); - - if (settings.contains("gamePath") && settings.value("gamePath").canConvert() && - !settings.value("gamePath").toString().isEmpty()) { - profile->gamePath = settings.value("gamePath").toString(); - } else { - profile->gamePath = getDefaultGamePath(); - } - - if (settings.contains("winePrefixPath") && settings.value("winePrefixPath").canConvert() && - !settings.value("winePrefixPath").toString().isEmpty()) { - profile->winePrefixPath = settings.value("winePrefixPath").toString(); - } else { - profile->winePrefixPath = getDefaultWinePrefixPath(); - } - - if (settings.contains("winePath") && settings.value("winePath").canConvert() && - !settings.value("winePath").toString().isEmpty()) { - profile->winePath = settings.value("winePath").toString(); - } - - ProfileSettings defaultSettings; - - // login - profile->encryptArguments = settings.value("encryptArguments", defaultSettings.encryptArguments).toBool(); - profile->isSapphire = settings.value("isSapphire", defaultSettings.isSapphire).toBool(); - profile->lobbyURL = settings.value("lobbyURL", defaultSettings.lobbyURL).toString(); - profile->rememberUsername = settings.value("rememberUsername", defaultSettings.rememberUsername).toBool(); - profile->rememberPassword = settings.value("rememberPassword", defaultSettings.rememberPassword).toBool(); - profile->rememberOTPSecret = settings.value("rememberOTPSecret", defaultSettings.rememberOTPSecret).toBool(); - profile->useOneTimePassword = settings.value("useOneTimePassword", defaultSettings.useOneTimePassword).toBool(); - profile->license = (GameLicense)settings.value("license", (int)defaultSettings.license).toInt(); - profile->isFreeTrial = settings.value("isFreeTrial", defaultSettings.isFreeTrial).toBool(); - profile->autoLogin = settings.value("autoLogin", defaultSettings.autoLogin).toBool(); - - profile->useDX9 = settings.value("useDX9", defaultSettings.useDX9).toBool(); - - // wine - profile->wineType = (WineType)settings.value("wineType", (int)defaultSettings.wineType).toInt(); - profile->useEsync = settings.value("useEsync", defaultSettings.useEsync).toBool(); - - readWineInfo(*profile); - - if (gamescopeAvailable) - profile->useGamescope = settings.value("useGamescope", defaultSettings.useGamescope).toBool(); - - if (gamemodeAvailable) - profile->useGamemode = settings.value("useGamemode", defaultSettings.useGamemode).toBool(); - - profile->enableDXVKhud = settings.value("enableDXVKhud", defaultSettings.enableDXVKhud).toBool(); - - profile->enableWatchdog = settings.value("enableWatchdog", defaultSettings.enableWatchdog).toBool(); - - // gamescope - profile->gamescope.borderless = - settings.value("gamescopeBorderless", defaultSettings.gamescope.borderless).toBool(); - profile->gamescope.width = settings.value("gamescopeWidth", defaultSettings.gamescope.width).toInt(); - profile->gamescope.height = settings.value("gamescopeHeight", defaultSettings.gamescope.height).toInt(); - profile->gamescope.refreshRate = - settings.value("gamescopeRefreshRate", defaultSettings.gamescope.refreshRate).toInt(); - - profile->dalamud.enabled = settings.value("enableDalamud", defaultSettings.dalamud.enabled).toBool(); - profile->dalamud.optOutOfMbCollection = - settings.value("dalamudOptOut", defaultSettings.dalamud.optOutOfMbCollection).toBool(); - profile->dalamud.channel = - (DalamudChannel)settings.value("dalamudChannel", (int)defaultSettings.dalamud.channel).toInt(); - - profileSettings[settings.value("index").toInt()] = profile; - - settings.endGroup(); - } - - readGameVersion(); -} - -void LauncherCore::readWineInfo(ProfileSettings& profile) { -#if defined(Q_OS_MAC) - switch (profile.wineType) { - case WineType::System: // system wine - profile.winePath = "/usr/local/bin/wine64"; - break; - case WineType::Custom: // custom path - profile.winePath = profile.winePath; - break; - case WineType::Builtin: // ffxiv built-in (for mac users) - profile.winePath = "/Applications/FINAL FANTASY XIV " - "ONLINE.app/Contents/SharedSupport/finalfantasyxiv/FINAL FANTASY XIV ONLINE/wine"; - break; - case WineType::XIVOnMac: - profile.winePath = "/Applications/XIV on Mac.app/Contents/Resources/wine/bin/wine64"; - break; - } -#endif - -#if defined(Q_OS_LINUX) - switch (profile.wineType) { - case WineType::System: // system wine (should be in $PATH) - profile.winePath = "/usr/bin/wine"; - break; - case WineType::Custom: // custom pth - profile.winePath = profile.winePath; - break; - } -#endif - -#if defined(Q_OS_LINUX) || defined(Q_OS_MAC) - auto wineProcess = new QProcess(this); - wineProcess->setProcessChannelMode(QProcess::MergedChannels); - - connect(wineProcess, &QProcess::readyRead, this, [wineProcess, &profile] { - profile.wineVersion = wineProcess->readAllStandardOutput().trimmed(); - }); - - launchExecutable(profile, wineProcess, {"--version"}, false, false); - - wineProcess->waitForFinished(); -#endif -} - -void LauncherCore::readGameVersion() { - for (auto& profile : profileSettings) { - profile->gameData = physis_gamedata_initialize((profile->gamePath + "/game").toStdString().c_str()); - profile->bootData = physis_bootdata_initialize((profile->gamePath + "/boot").toStdString().c_str()); - - if(profile->bootData != nullptr) { - profile->bootVersion = physis_bootdata_get_version(profile->bootData); - } - - if(profile->gameData != nullptr) { - profile->repositories = physis_gamedata_get_repositories(profile->gameData); - readGameData(*profile); - } - } -} - -LauncherCore::LauncherCore(bool isSteam) - : settings(QSettings::IniFormat, QSettings::UserScope, QCoreApplication::applicationName()), isSteam(isSteam) { - mgr = new QNetworkAccessManager(); - sapphireLauncher = new SapphireLauncher(*this); - squareLauncher = new SquareLauncher(*this); - squareBoot = new SquareBoot(*this, *squareLauncher); - assetUpdater = new AssetUpdater(*this); - steamApi = new SteamAPI(*this); - -#ifdef ENABLE_WATCHDOG - watchdog = new Watchdog(*this); -#endif - - readInitialInformation(); - - steamApi->setLauncherMode(true); -} - -ProfileSettings& LauncherCore::getProfile(int index) { - return *profileSettings[index]; -} - -int LauncherCore::getProfileIndex(const QString& name) { - for (int i = 0; i < profileSettings.size(); i++) { - if (profileSettings[i]->name == name) - return i; - } - - return -1; -} - -QList LauncherCore::profileList() const { - QList list; - for (auto profile : profileSettings) { - list.append(profile->name); - } - - return list; -} - -int LauncherCore::addProfile() { - auto newProfile = new ProfileSettings(); - newProfile->uuid = QUuid::createUuid(); - newProfile->name = "New Profile"; - - readWineInfo(*newProfile); - - newProfile->gamePath = getDefaultGamePath(); - newProfile->winePrefixPath = getDefaultWinePrefixPath(); - - profileSettings.append(newProfile); - - settingsChanged(); - - return profileSettings.size() - 1; -} - -int LauncherCore::deleteProfile(const QString& name) { - int index = 0; - for (int i = 0; i < profileSettings.size(); i++) { - if (profileSettings[i]->name == name) - index = i; - } - - // remove group so it doesnt stay - settings.beginGroup(profileSettings[index]->uuid.toString(QUuid::StringFormat::WithoutBraces)); - settings.remove(""); - settings.endGroup(); - - profileSettings.removeAt(index); - - return index - 1; -} - -void LauncherCore::saveSettings() { - settings.setValue("defaultProfile", defaultProfileIndex); - settings.setValue("closeWhenLaunched", appSettings.closeWhenLaunched); - settings.setValue("showBanners", appSettings.showBanners); - settings.setValue("showNewsList", appSettings.showNewsList); - - for (int i = 0; i < profileSettings.size(); i++) { - const auto& profile = profileSettings[i]; - - settings.beginGroup(profile->uuid.toString(QUuid::StringFormat::WithoutBraces)); - - settings.setValue("name", profile->name); - settings.setValue("index", i); - - // game - settings.setValue("useDX9", profile->useDX9); - settings.setValue("gamePath", profile->gamePath); - - // wine - settings.setValue("wineType", (int)profile->wineType); - settings.setValue("winePath", profile->winePath); - settings.setValue("winePrefixPath", profile->winePrefixPath); - - settings.setValue("useEsync", profile->useEsync); - settings.setValue("useGamescope", profile->useGamescope); - settings.setValue("useGamemode", profile->useGamemode); - - // gamescope - settings.setValue("gamescopeFullscreen", profile->gamescope.fullscreen); - settings.setValue("gamescopeBorderless", profile->gamescope.borderless); - settings.setValue("gamescopeWidth", profile->gamescope.width); - settings.setValue("gamescopeHeight", profile->gamescope.height); - settings.setValue("gamescopeRefreshRate", profile->gamescope.refreshRate); - - // login - settings.setValue("encryptArguments", profile->encryptArguments); - settings.setValue("isSapphire", profile->isSapphire); - settings.setValue("lobbyURL", profile->lobbyURL); - settings.setValue("rememberUsername", profile->rememberUsername); - settings.setValue("rememberPassword", profile->rememberPassword); - settings.setValue("rememberOTPSecret", profile->rememberOTPSecret); - settings.setValue("useOneTimePassword", profile->useOneTimePassword); - settings.setValue("license", (int)profile->license); - settings.setValue("isFreeTrial", profile->isFreeTrial); - settings.setValue("autoLogin", profile->autoLogin); - - settings.setValue("enableDalamud", profile->dalamud.enabled); - settings.setValue("dalamudOptOut", profile->dalamud.optOutOfMbCollection); - settings.setValue("dalamudChannel", (int)profile->dalamud.channel); - settings.setValue("enableWatchdog", profile->enableWatchdog); - - settings.endGroup(); - } -} - -bool LauncherCore::checkIfInPath(const QString& program) { - // TODO: also check /usr/local/bin, /bin32 etc (basically read $PATH) - const QString directory = "/usr/bin"; - - QFileInfo fileInfo(directory + "/" + program); - - return fileInfo.exists() && fileInfo.isFile(); -} - -QString LauncherCore::getDefaultWinePrefixPath() { -#if defined(Q_OS_MACOS) - return QDir::homePath() + "/Library/Application Support/FINAL FANTASY XIV ONLINE/Bottles/published_Final_Fantasy"; -#endif - -#if defined(Q_OS_LINUX) - return QDir::homePath() + "/.wine"; -#endif - - return ""; -} - -QString LauncherCore::getDefaultGamePath() { -#if defined(Q_OS_WIN) - return "C:\\Program Files (x86)\\SquareEnix\\FINAL FANTASY XIV - A Realm Reborn"; -#endif - -#if defined(Q_OS_MAC) - return QDir::homePath() + - "/Library/Application Support/FINAL FANTASY XIV ONLINE/Bottles/published_Final_Fantasy/drive_c/Program " - "Files (x86)/SquareEnix/FINAL FANTASY XIV - A Realm Reborn"; -#endif - -#if defined(Q_OS_LINUX) - return QDir::homePath() + "/.wine/drive_c/Program Files (x86)/SquareEnix/FINAL FANTASY XIV - A Realm Reborn"; -#endif -} - -void LauncherCore::addRegistryKey(const ProfileSettings& settings, QString key, QString value, QString data) { - auto process = new QProcess(this); - process->setProcessEnvironment(QProcessEnvironment::systemEnvironment()); - launchExecutable(settings, process, {"reg", "add", std::move(key), "/v", std::move(value), "/d", std::move(data), "/f"}, false, false); -} - -void LauncherCore::readGameData(ProfileSettings& profile) { - physis_EXH* exh = physis_gamedata_read_excel_sheet_header(profile.gameData, "ExVersion"); - if (exh != nullptr) { - physis_EXD exd = physis_gamedata_read_excel_sheet(profile.gameData, "ExVersion", exh, Language::English, 0); - - for (int i = 0; i < exd.row_count; i++) { - expansionNames.push_back(exd.row_data[i].column_data[0].string._0); - } - - physis_gamedata_free_sheet(exd); - physis_gamedata_free_sheet_header(exh); - } -} - -void LauncherCore::login(LoginInformation* loginInformation) { - if (loginInformation->settings->isSapphire) { - sapphireLauncher->login(loginInformation->settings->lobbyURL, *loginInformation); - } else { - squareBoot->checkGateStatus(loginInformation); - } -} - -bool LauncherCore::autoLogin(ProfileSettings& profile) { - QString username = profile.getKeychainValue("username"); - QString password = profile.getKeychainValue("password"); - QString otpSecret = profile.getKeychainValue("otpsecret"); - - auto info = new LoginInformation(); - info->settings = &profile; - info->username = username; - info->password = password; - - if(profile.useOneTimePassword && !profile.rememberOTPSecret) - return false; - - if(profile.useOneTimePassword && profile.rememberOTPSecret) { - // generate otp - char* totp = get_totp (otpSecret.toStdString().c_str(), 6, 30, SHA1, nullptr); - info->oneTimePassword = totp; - free (totp); - } - - // TODO: when login fails, we need some way to propagate this back? or not? - login(info); - - return true; -} - -void ProfileSettings::setKeychainValue(const QString& key, const QString& value) const { - auto job = new QKeychain::WritePasswordJob("Astra"); - job->setTextData(value); - job->setKey(name + "-" + key); - job->start(); -} - -QString ProfileSettings::getKeychainValue(const QString& key) const { - auto loop = new QEventLoop(); - - auto job = new QKeychain::ReadPasswordJob("Astra"); - job->setKey(name + "-" + key); - job->start(); - - QString value; - - QObject::connect( - job, &QKeychain::ReadPasswordJob::finished, [loop, job, &value](QKeychain::Job* j) { - value = job->textData(); - loop->quit(); - }); - - loop->exec(); - - return value; -} diff --git a/launcher/desktop/CMakeLists.txt b/launcher/desktop/CMakeLists.txt deleted file mode 100644 index 01088d4..0000000 --- a/launcher/desktop/CMakeLists.txt +++ /dev/null @@ -1,25 +0,0 @@ -set(HEADERS - include/aboutwindow.h - include/bannerwidget.h - include/desktopinterface.h - include/gamescopesettingswindow.h - include/launcherwindow.h - include/settingswindow.h - include/autologinwindow.h) - -set(SRC - src/aboutwindow.cpp - src/bannerwidget.cpp - src/desktopinterface.cpp - src/gamescopesettingswindow.cpp - src/launcherwindow.cpp - src/settingswindow.cpp - src/autologinwindow.cpp include/virtualwindow.h src/virtualwindow.cpp src/virtualdialog.cpp include/virtualdialog.h) - -add_library(astra_desktop STATIC ${HEADERS} ${SRC}) -target_include_directories(astra_desktop PUBLIC include) -target_link_libraries(astra_desktop PUBLIC - astra_core - Qt5::Core - Qt5::Widgets - Qt5::Network) \ No newline at end of file diff --git a/launcher/desktop/include/aboutwindow.h b/launcher/desktop/include/aboutwindow.h deleted file mode 100644 index 85f067a..0000000 --- a/launcher/desktop/include/aboutwindow.h +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once - -#include "virtualdialog.h" - -class AboutWindow : public VirtualDialog { -public: - explicit AboutWindow(DesktopInterface& interface, QWidget* widget = nullptr); -}; \ No newline at end of file diff --git a/launcher/desktop/include/autologinwindow.h b/launcher/desktop/include/autologinwindow.h deleted file mode 100644 index f358029..0000000 --- a/launcher/desktop/include/autologinwindow.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include "virtualdialog.h" - -class LauncherCore; -class LauncherWindow; -struct ProfileSettings; - -class AutoLoginWindow : public VirtualDialog { - Q_OBJECT -public: - AutoLoginWindow(DesktopInterface& interface, ProfileSettings& settings, LauncherCore& core, QWidget* parent = nullptr); - -signals: - void loginCanceled(); -}; diff --git a/launcher/desktop/include/bannerwidget.h b/launcher/desktop/include/bannerwidget.h deleted file mode 100644 index 2bafa9d..0000000 --- a/launcher/desktop/include/bannerwidget.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include -#include - -class BannerWidget : public QLabel { -public: - BannerWidget(); - - void setUrl(QUrl url); - -protected: - void mousePressEvent(QMouseEvent* event) override; - -private: - QUrl url; -}; \ No newline at end of file diff --git a/launcher/desktop/include/desktopinterface.h b/launcher/desktop/include/desktopinterface.h deleted file mode 100644 index 19af55a..0000000 --- a/launcher/desktop/include/desktopinterface.h +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once - -#include -#include - -#include "launcherwindow.h" -#include "autologinwindow.h" -#include "virtualdialog.h" - -/* - * The desktop, mouse and keyboard-driven interface for Astra. Primarily meant - * for regular desktop usage. - */ -class DesktopInterface { -public: - explicit DesktopInterface(LauncherCore& core); - - void addWindow(VirtualWindow* window); - void addDialog(VirtualDialog* dialog); - - bool oneWindow = false; - bool isSteamDeck = false; - -private: - QMdiArea* mdiArea = nullptr; - QMainWindow* mdiWindow = nullptr; - - LauncherWindow* window = nullptr; - AutoLoginWindow* autoLoginWindow = nullptr; -}; \ No newline at end of file diff --git a/launcher/desktop/include/gamescopesettingswindow.h b/launcher/desktop/include/gamescopesettingswindow.h deleted file mode 100644 index 5cc6c47..0000000 --- a/launcher/desktop/include/gamescopesettingswindow.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include "virtualdialog.h" - -class LauncherCore; -class LauncherWindow; -struct ProfileSettings; - -class GamescopeSettingsWindow : public VirtualDialog { -public: - GamescopeSettingsWindow(DesktopInterface& interface, ProfileSettings& settings, LauncherCore& core, QWidget* parent = nullptr); -}; diff --git a/launcher/desktop/include/launcherwindow.h b/launcher/desktop/include/launcherwindow.h deleted file mode 100644 index 973e5b3..0000000 --- a/launcher/desktop/include/launcherwindow.h +++ /dev/null @@ -1,67 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "headline.h" -#include "launchercore.h" -#include "virtualwindow.h" - -class DesktopInterface; - -class LauncherWindow : public VirtualWindow { - Q_OBJECT -public: - explicit LauncherWindow(DesktopInterface& interface, LauncherCore& new_headline, QWidget* parent = nullptr); - - ProfileSettings& currentProfile(); - - void openPath(const QString& path); - -public slots: - void reloadControls(); - -private: - void reloadNews(); - - LauncherCore& core; - - Headline headline; - - bool currentlyReloadingControls = false; - - QGridLayout* layout; - QFormLayout* loginLayout; - - QScrollArea* bannerScrollArea; - QWidget* bannerParentWidget; - QHBoxLayout* bannerLayout; - QTreeWidget* newsListView; - QTimer* bannerTimer = nullptr; - int currentBanner = 0; - - std::vector bannerWidgets; - - QAction* launchOfficial; - QAction* launchSysInfo; - QAction* launchCfgBackup; - QAction* openGameDir; - - QComboBox* profileSelect; - QLineEdit *usernameEdit, *passwordEdit; - QLineEdit* otpEdit; - QCheckBox *rememberUsernameBox, *rememberPasswordBox; - QPushButton *loginButton, *registerButton; - -#if defined(Q_OS_MAC) || defined(Q_OS_LINUX) - QAction* wineCfg; -#endif - - DesktopInterface& interface; -}; \ No newline at end of file diff --git a/launcher/desktop/include/settingswindow.h b/launcher/desktop/include/settingswindow.h deleted file mode 100644 index d0b9d50..0000000 --- a/launcher/desktop/include/settingswindow.h +++ /dev/null @@ -1,89 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "virtualdialog.h" - -class LauncherCore; -class LauncherWindow; -struct ProfileSettings; - -class SettingsWindow : public VirtualDialog { -public: - SettingsWindow(DesktopInterface& interface, int defaultTab, LauncherWindow& window, LauncherCore& core, QWidget* parent = nullptr); - -public slots: - void reloadControls(); - -private: - void setupAccountsTab(QGridLayout& layout); - - // profile specific tabs - void setupGameTab(QFormLayout& layout); - void setupLoginTab(QFormLayout& layout); - void setupWineTab(QFormLayout& layout); - void setupDalamudTab(QFormLayout& layout); - - ProfileSettings& getCurrentProfile(); - - QListWidget* profileWidget = nullptr; - QPushButton* deleteAccountButton = nullptr; - - QListWidget* accountWidget = nullptr; - QPushButton* removeAccountButton = nullptr; - - // general - QCheckBox* closeWhenLaunched = nullptr; - QCheckBox* showBanner = nullptr; - QCheckBox* showNewsList = nullptr; - - // game - QLineEdit* nameEdit = nullptr; - QComboBox* directXCombo = nullptr; - QLabel* currentGameDirectory = nullptr; - QLabel* expansionVersionLabel = nullptr; - QPushButton* gameDirectoryButton = nullptr; - - // wine - QComboBox* wineTypeCombo; - QPushButton* selectWineButton; - QLabel* winePathLabel; - QLabel* winePrefixDirectory; - QPushButton* configureGamescopeButton; - QLabel* wineVersionLabel; - - QCheckBox *useGamescope, *useEsync, *useGamemode; - QCheckBox* enableWatchdog; - - // login - QCheckBox* encryptArgumentsBox = nullptr; - QComboBox* serverType = nullptr; - QLineEdit* lobbyServerURL = nullptr; - QCheckBox *rememberUsernameBox = nullptr, *rememberPasswordBox = nullptr, *rememberOTPSecretBox = nullptr; - QPushButton* otpSecretButton = nullptr; - QComboBox* gameLicenseBox = nullptr; - QCheckBox* freeTrialBox = nullptr; - QCheckBox* useOneTimePassword = nullptr; - QCheckBox* autoLoginBox = nullptr; - - // dalamud - QCheckBox* enableDalamudBox = nullptr; - QLabel* dalamudVersionLabel = nullptr; - QLabel* dalamudAssetVersionLabel = nullptr; - QCheckBox* dalamudOptOutBox = nullptr; - QComboBox* dalamudChannel = nullptr; - - bool currentlyReloadingControls = false; - - LauncherWindow& window; - LauncherCore& core; - - DesktopInterface& interface; -}; diff --git a/launcher/desktop/include/virtualdialog.h b/launcher/desktop/include/virtualdialog.h deleted file mode 100644 index 9473530..0000000 --- a/launcher/desktop/include/virtualdialog.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include -#include -#include - -class DesktopInterface; - -class VirtualDialog : public QObject { - Q_OBJECT -public: - explicit VirtualDialog(DesktopInterface& interface, QWidget* parent = nullptr); - - void setWindowTitle(const QString& title); - void show(); - void hide(); - void close(); - void setWindowModality(Qt::WindowModality modality); - void setLayout(QLayout* layout); - - QWidget* getRootWidget(); - - QMdiSubWindow* mdi_window = nullptr; - QDialog* normal_dialog = nullptr; - -private: - DesktopInterface& interface; -}; \ No newline at end of file diff --git a/launcher/desktop/include/virtualwindow.h b/launcher/desktop/include/virtualwindow.h deleted file mode 100644 index c2ab01b..0000000 --- a/launcher/desktop/include/virtualwindow.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include -#include -#include - -class DesktopInterface; - -class VirtualWindow : public QObject { - Q_OBJECT -public: - explicit VirtualWindow(DesktopInterface& interface, QWidget* parent = nullptr); - - void setWindowTitle(const QString& title); - void setCentralWidget(QWidget* widget); - void show(); - void showMaximized(); - void hide(); - - QMenuBar* menuBar(); - - QWidget* getRootWidget(); - - QMdiSubWindow* mdi_window = nullptr; - QMainWindow* normal_window = nullptr; - -private: - DesktopInterface& interface; -}; \ No newline at end of file diff --git a/launcher/desktop/src/aboutwindow.cpp b/launcher/desktop/src/aboutwindow.cpp deleted file mode 100644 index 26f59af..0000000 --- a/launcher/desktop/src/aboutwindow.cpp +++ /dev/null @@ -1,78 +0,0 @@ -#include "aboutwindow.h" - -#include -#include -#include -#include - -#include "config.h" -#include "license.h" - -AboutWindow::AboutWindow(DesktopInterface& interface, QWidget* widget) : VirtualDialog(interface, widget) { - setWindowTitle("About"); - setWindowModality(Qt::WindowModality::ApplicationModal); - - auto mainLayout = new QVBoxLayout(); - setLayout(mainLayout); - - auto mainLabel = new QLabel(); - mainLabel->setText(QString("

Astra

\nVersion %1").arg(version)); - mainLayout->addWidget(mainLabel); - - auto aboutWidget = new QWidget(); - auto aboutLayout = new QVBoxLayout(); - aboutWidget->setLayout(aboutLayout); - - auto aboutLabel = new QLabel(); - aboutLabel->setText("Cross-platform FFXIV launcher"); - aboutLayout->addWidget(aboutLabel); - - auto websiteLabel = new QLabel(); - websiteLabel->setText("https://xiv.zone/astra"); - websiteLabel->setOpenExternalLinks(true); - aboutLayout->addWidget(websiteLabel); - - auto licenseLabel = new QLabel(); - licenseLabel->setText("License: GNU General Public License Version 3"); - connect(licenseLabel, &QLabel::linkActivated, [&interface] { - auto licenseDialog = new VirtualDialog(interface); - licenseDialog->setWindowTitle("License Agreement"); - - auto layout = new QVBoxLayout(); - licenseDialog->setLayout(layout); - - auto licenseEdit = new QPlainTextEdit(); - licenseEdit->setPlainText(license); - licenseEdit->setReadOnly(true); - layout->addWidget(licenseEdit); - - licenseDialog->show(); - }); - aboutLayout->addWidget(licenseLabel); - - aboutLayout->addStretch(); - - auto authorsWidget = new QWidget(); - auto authorsLayout = new QVBoxLayout(); - authorsWidget->setLayout(authorsLayout); - - auto authorNameLabel = new QLabel(); - authorNameLabel->setText("Joshua Goins"); - - QFont boldFont = authorNameLabel->font(); - boldFont.setBold(true); - authorNameLabel->setFont(boldFont); - - authorsLayout->addWidget(authorNameLabel); - - auto authorRoleLabel = new QLabel(); - authorRoleLabel->setText("Maintainer"); - authorsLayout->addWidget(authorRoleLabel); - - authorsLayout->addStretch(); - - auto tabWidget = new QTabWidget(); - tabWidget->addTab(aboutWidget, "About"); - tabWidget->addTab(authorsWidget, "Authors"); - mainLayout->addWidget(tabWidget); -} \ No newline at end of file diff --git a/launcher/desktop/src/autologinwindow.cpp b/launcher/desktop/src/autologinwindow.cpp deleted file mode 100644 index 33df08c..0000000 --- a/launcher/desktop/src/autologinwindow.cpp +++ /dev/null @@ -1,45 +0,0 @@ -#include "autologinwindow.h" - -#include -#include -#include -#include -#include -#include - -#include "launchercore.h" -#include "launcherwindow.h" -#include "sapphirelauncher.h" - -AutoLoginWindow::AutoLoginWindow(DesktopInterface& interface, ProfileSettings& profile, LauncherCore& core, QWidget* parent) - : VirtualDialog(interface, parent) { - setWindowTitle("Auto Login"); - setWindowModality(Qt::WindowModality::ApplicationModal); - - auto mainLayout = new QFormLayout(); - setLayout(mainLayout); - - auto label = new QLabel("Currently logging in..."); - mainLayout->addWidget(label); - - auto cancelButton = new QPushButton("Cancel"); - connect(cancelButton, &QPushButton::clicked, this, &AutoLoginWindow::loginCanceled); - mainLayout->addWidget(cancelButton); - - auto autologinTimer = new QTimer(); - - connect(autologinTimer, &QTimer::timeout, [&, this, autologinTimer] { - core.autoLogin(profile); - }); - - connect(this, &AutoLoginWindow::loginCanceled, [autologinTimer] { - autologinTimer->stop(); - }); - - connect(&core, &LauncherCore::successfulLaunch, [this, autologinTimer] { - close(); - autologinTimer->stop(); - }); - - autologinTimer->start(5000); -} \ No newline at end of file diff --git a/launcher/desktop/src/bannerwidget.cpp b/launcher/desktop/src/bannerwidget.cpp deleted file mode 100644 index 65deafc..0000000 --- a/launcher/desktop/src/bannerwidget.cpp +++ /dev/null @@ -1,18 +0,0 @@ -#include "bannerwidget.h" - -#include -#include -#include - -BannerWidget::BannerWidget() : QLabel() { - setCursor(Qt::CursorShape::PointingHandCursor); -} - -void BannerWidget::mousePressEvent(QMouseEvent* event) { - qDebug() << "Clicked!"; - QDesktopServices::openUrl(url); -} - -void BannerWidget::setUrl(QUrl newUrl) { - this->url = std::move(newUrl); -} diff --git a/launcher/desktop/src/desktopinterface.cpp b/launcher/desktop/src/desktopinterface.cpp deleted file mode 100644 index e7718da..0000000 --- a/launcher/desktop/src/desktopinterface.cpp +++ /dev/null @@ -1,96 +0,0 @@ -#include "desktopinterface.h" -#include "autologinwindow.h" -#include "gameinstaller.h" - -DesktopInterface::DesktopInterface(LauncherCore& core) { - if(oneWindow) { - mdiArea = new QMdiArea(); - mdiWindow = new QMainWindow(); - - if(isSteamDeck) { - mdiWindow->setWindowFlag(Qt::FramelessWindowHint); - mdiWindow->setFixedSize(1280, 800); - } - - mdiWindow->setWindowTitle("Combined Interface"); - mdiWindow->setCentralWidget(mdiArea); - mdiWindow->show(); - } - - window = new LauncherWindow(*this, core); - - auto& defaultProfile = core.getProfile(core.defaultProfileIndex); - - if (!defaultProfile.isGameInstalled()) { - auto messageBox = new QMessageBox(); - messageBox->setIcon(QMessageBox::Icon::Question); - messageBox->setText("No Game Found"); - messageBox->setInformativeText("FFXIV is not installed. Would you like to install it now?"); - - QString detailedText = - QString("Astra will install FFXIV for you at '%1'").arg(core.getProfile(core.defaultProfileIndex).gamePath); - detailedText.append( - "\n\nIf you do not wish to install it to this location, please set it in your default profile first."); - - messageBox->setDetailedText(detailedText); - messageBox->setWindowModality(Qt::WindowModal); - - auto installButton = messageBox->addButton("Install Game", QMessageBox::YesRole); - QObject::connect(installButton, &QPushButton::clicked, [&core, messageBox] { - installGame(core, core.getProfile(core.defaultProfileIndex), [messageBox, &core] { - core.readGameVersion(); - - messageBox->close(); - }); - }); - - messageBox->addButton(QMessageBox::StandardButton::No); - messageBox->setDefaultButton(installButton); - - messageBox->exec(); - } - -#if defined(Q_OS_LINUX) || defined(Q_OS_MAC) - if (!core.isSteam && !defaultProfile.isWineInstalled()) { - auto messageBox = new QMessageBox(); - messageBox->setIcon(QMessageBox::Icon::Critical); - messageBox->setAttribute(Qt::WA_DeleteOnClose); - messageBox->setText("No Wine Found"); - messageBox->setInformativeText("Wine is not installed but is required to FFXIV on this operating system."); - //messageBox->setWindowModality(Qt::WindowModal); - - messageBox->addButton(QMessageBox::StandardButton::Ok); - messageBox->setDefaultButton(QMessageBox::StandardButton::Ok); - - messageBox->show(); - } -#endif - - if(defaultProfile.autoLogin) { - autoLoginWindow = new AutoLoginWindow(*this, defaultProfile, core); - autoLoginWindow->show(); - - QObject::connect(autoLoginWindow, &AutoLoginWindow::loginCanceled,[=] { - autoLoginWindow->hide(); - window->show(); - }); - } else { - if(oneWindow) { - window->showMaximized(); - } else { - window->show(); - } - } -} - -void DesktopInterface::addWindow(VirtualWindow* window) { - if(oneWindow) { - mdiArea->addSubWindow(window->mdi_window); - } -} - -void DesktopInterface::addDialog(VirtualDialog* dialog) { - if(oneWindow) { - mdiArea->addSubWindow(dialog->mdi_window); - } -} \ No newline at end of file diff --git a/launcher/desktop/src/gamescopesettingswindow.cpp b/launcher/desktop/src/gamescopesettingswindow.cpp deleted file mode 100644 index eb977bb..0000000 --- a/launcher/desktop/src/gamescopesettingswindow.cpp +++ /dev/null @@ -1,67 +0,0 @@ -#include "gamescopesettingswindow.h" - -#include -#include -#include -#include -#include -#include - -#include "launchercore.h" - -GamescopeSettingsWindow::GamescopeSettingsWindow(DesktopInterface& interface, ProfileSettings& settings, LauncherCore& core, QWidget* parent) - : VirtualDialog(interface, parent) { - setWindowTitle("Gamescope Settings"); - setWindowModality(Qt::WindowModality::ApplicationModal); - - auto mainLayout = new QFormLayout(); - setLayout(mainLayout); - - auto fullscreenBox = new QCheckBox("Fullscreen"); - fullscreenBox->setChecked(settings.gamescope.fullscreen); - connect(fullscreenBox, &QCheckBox::clicked, [&](bool checked) { - settings.gamescope.fullscreen = checked; - - core.saveSettings(); - }); - mainLayout->addWidget(fullscreenBox); - - auto borderlessBox = new QCheckBox("Borderless"); - borderlessBox->setChecked(settings.gamescope.fullscreen); - connect(borderlessBox, &QCheckBox::clicked, [&](bool checked) { - settings.gamescope.borderless = checked; - - core.saveSettings(); - }); - mainLayout->addWidget(borderlessBox); - - auto widthBox = new QSpinBox(); - widthBox->setValue(settings.gamescope.width); - widthBox->setSpecialValueText("Default"); - connect(widthBox, QOverload::of(&QSpinBox::valueChanged), [&](int value) { - settings.gamescope.width = value; - - core.saveSettings(); - }); - mainLayout->addRow("Width", widthBox); - - auto heightBox = new QSpinBox(); - heightBox->setValue(settings.gamescope.height); - heightBox->setSpecialValueText("Default"); - connect(heightBox, QOverload::of(&QSpinBox::valueChanged), [&](int value) { - settings.gamescope.height = value; - - core.saveSettings(); - }); - mainLayout->addRow("Height", heightBox); - - auto refreshRateBox = new QSpinBox(); - refreshRateBox->setValue(settings.gamescope.refreshRate); - refreshRateBox->setSpecialValueText("Default"); - connect(refreshRateBox, QOverload::of(&QSpinBox::valueChanged), [&](int value) { - settings.gamescope.refreshRate = value; - - core.saveSettings(); - }); - mainLayout->addRow("Refresh Rate", refreshRateBox); -} \ No newline at end of file diff --git a/launcher/desktop/src/launcherwindow.cpp b/launcher/desktop/src/launcherwindow.cpp deleted file mode 100644 index b9b2948..0000000 --- a/launcher/desktop/src/launcherwindow.cpp +++ /dev/null @@ -1,556 +0,0 @@ -#include "launcherwindow.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "aboutwindow.h" -#include "assetupdater.h" -#include "bannerwidget.h" -#include "encryptedarg.h" -#include "gameinstaller.h" -#include "headline.h" -#include "sapphirelauncher.h" -#include "settingswindow.h" -#include "squarelauncher.h" -#include "desktopinterface.h" - -LauncherWindow::LauncherWindow(DesktopInterface& interface, LauncherCore& core, QWidget* parent) : VirtualWindow(interface, parent), core(core), interface(interface) { - setWindowTitle("Astra"); - - connect(&core, &LauncherCore::settingsChanged, this, &LauncherWindow::reloadControls); - - QMenu* toolsMenu = menuBar()->addMenu("Tools"); - - launchOfficial = toolsMenu->addAction("Open Official Launcher..."); - launchOfficial->setIcon(QIcon::fromTheme("application-x-executable")); - connect(launchOfficial, &QAction::triggered, [=] { - struct Argument { - QString key, value; - }; - - QString executeArg("%1%2%3%4"); - QDateTime dateTime = QDateTime::currentDateTime(); - executeArg = executeArg.arg(dateTime.date().month() + 1, 2, 10, QLatin1Char('0')); - executeArg = executeArg.arg(dateTime.date().day(), 2, 10, QLatin1Char('0')); - 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 = currentProfile().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)}); - - const QString argFormat = " /%1 =%2"; - - QString argJoined; - for (auto& arg : arguments) { - argJoined += argFormat.arg(arg.key, arg.value.replace(" ", " ")); - } - - QString finalArg = encryptGameArg(argJoined); - - auto launcherProcess = new QProcess(); - this->core.launchExecutable(currentProfile(), - launcherProcess, - {currentProfile().gamePath + "/boot/ffxivlauncher64.exe", finalArg}, - false, - true); - }); - - launchSysInfo = toolsMenu->addAction("Open System Info..."); - launchSysInfo->setIcon(QIcon::fromTheme("application-x-executable")); - connect(launchSysInfo, &QAction::triggered, [=] { - auto sysinfoProcess = new QProcess(); - this->core.launchExecutable(currentProfile(), - sysinfoProcess, - {currentProfile().gamePath + "/boot/ffxivsysinfo64.exe"}, - false, - false); - }); - - launchCfgBackup = toolsMenu->addAction("Open Config Backup..."); - launchCfgBackup->setIcon(QIcon::fromTheme("application-x-executable")); - connect(launchCfgBackup, &QAction::triggered, [=] { - auto configProcess = new QProcess(); - this->core.launchExecutable(currentProfile(), - configProcess, - {currentProfile().gamePath + "/boot/ffxivconfig64.exe"}, - false, - false); - }); - - toolsMenu->addSeparator(); - - openGameDir = toolsMenu->addAction("Open Game Directory..."); - openGameDir->setIcon(QIcon::fromTheme("document-open")); - connect(openGameDir, &QAction::triggered, [=] { - openPath(currentProfile().gamePath); - }); - - QMenu* gameMenu = menuBar()->addMenu("Game"); - - auto installGameAction = gameMenu->addAction("Install game..."); - connect(installGameAction, &QAction::triggered, [this] { - // TODO: lol duplication - auto messageBox = new QMessageBox(); - messageBox->setIcon(QMessageBox::Icon::Question); - messageBox->setText("Warning"); - messageBox->setInformativeText("FFXIV will be installed to your selected game directory."); - - QString detailedText = QString("Astra will install FFXIV for you at '%1'").arg(this->currentProfile().gamePath); - detailedText.append( - "\n\nIf you do not wish to install it to this location, please change your profile settings."); - - messageBox->setDetailedText(detailedText); - messageBox->setWindowModality(Qt::WindowModal); - - auto installButton = messageBox->addButton("Install Game", QMessageBox::YesRole); - connect(installButton, &QPushButton::clicked, [this, messageBox] { - installGame(this->core, this->currentProfile(), [this, messageBox] { - this->core.readGameVersion(); - - messageBox->close(); - }); - }); - - messageBox->addButton(QMessageBox::StandardButton::No); - messageBox->setDefaultButton(installButton); - - messageBox->exec(); - }); - - QMenu* fileMenu = menuBar()->addMenu("Settings"); - - QAction* settingsAction = fileMenu->addAction("Configure Astra..."); - settingsAction->setIcon(QIcon::fromTheme("configure")); - settingsAction->setMenuRole(QAction::MenuRole::PreferencesRole); - connect(settingsAction, &QAction::triggered, [=, &interface] { - auto window = new SettingsWindow(interface, 0, *this, this->core); - connect(&this->core, &LauncherCore::settingsChanged, window, &SettingsWindow::reloadControls); - window->show(); - }); - - QAction* profilesAction = fileMenu->addAction("Configure Profiles..."); - profilesAction->setIcon(QIcon::fromTheme("configure")); - profilesAction->setMenuRole(QAction::MenuRole::NoRole); - connect(profilesAction, &QAction::triggered, [=, &interface] { - auto window = new SettingsWindow(interface, 1, *this, this->core); - connect(&this->core, &LauncherCore::settingsChanged, window, &SettingsWindow::reloadControls); - window->show(); - }); - -#if defined(Q_OS_MAC) || defined(Q_OS_LINUX) - fileMenu->addSeparator(); - - wineCfg = fileMenu->addAction("Configure Wine..."); - wineCfg->setMenuRole(QAction::MenuRole::NoRole); - wineCfg->setIcon(QIcon::fromTheme("configure")); - connect(wineCfg, &QAction::triggered, [=] { - auto configProcess = new QProcess(); - this->core.launchExecutable(currentProfile(), - configProcess, - {"winecfg.exe"}, - false, - false); - }); -#endif - - QMenu* helpMenu = menuBar()->addMenu("Help"); - QAction* showAbout = helpMenu->addAction("About Astra"); - showAbout->setIcon(QIcon::fromTheme("help-about")); - connect(showAbout, &QAction::triggered, [=, &interface] { - auto window = new AboutWindow(interface); - window->show(); - }); - - QAction* showAboutQt = helpMenu->addAction("About Qt"); - showAboutQt->setIcon(QIcon::fromTheme("help-about")); - connect(showAboutQt, &QAction::triggered, [=] { - QMessageBox::aboutQt(nullptr); - }); - - layout = new QGridLayout(); - - bannerScrollArea = new QScrollArea(); - bannerLayout = new QHBoxLayout(); - bannerLayout->setContentsMargins(0, 0, 0, 0); - bannerLayout->setSpacing(0); - bannerLayout->setSizeConstraint(QLayout::SizeConstraint::SetMinAndMaxSize); - bannerParentWidget = new QWidget(); - bannerParentWidget->setFixedHeight(250); - bannerScrollArea->setFixedWidth(640); - bannerScrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - bannerScrollArea->verticalScrollBar()->setEnabled(false); - bannerScrollArea->horizontalScrollBar()->setEnabled(false); - - bannerScrollArea->setWidget(bannerParentWidget); - bannerParentWidget->setLayout(bannerLayout); - - newsListView = new QTreeWidget(); - newsListView->setColumnCount(2); - newsListView->setHeaderLabels({"Title", "Date"}); - connect(newsListView, &QTreeWidget::itemClicked, [](QTreeWidgetItem* item, int column) { - auto url = item->data(0, Qt::UserRole).toUrl(); - qInfo() << "clicked" << url; - QDesktopServices::openUrl(url); - }); - - loginLayout = new QFormLayout(); - layout->addLayout(loginLayout, 0, 1, 1, 1); - - profileSelect = new QComboBox(); - connect(profileSelect, static_cast(&QComboBox::currentIndexChanged), [=](int index) { - reloadControls(); - }); - - loginLayout->addRow("Profile", profileSelect); - - usernameEdit = new QLineEdit(); - loginLayout->addRow("Username", usernameEdit); - - rememberUsernameBox = new QCheckBox(); - connect(rememberUsernameBox, &QCheckBox::stateChanged, [=](int) { - currentProfile().rememberUsername = rememberUsernameBox->isChecked(); - this->core.saveSettings(); - }); - loginLayout->addRow("Remember Username?", rememberUsernameBox); - - passwordEdit = new QLineEdit(); - passwordEdit->setEchoMode(QLineEdit::EchoMode::Password); - loginLayout->addRow("Password", passwordEdit); - - rememberPasswordBox = new QCheckBox(); - connect(rememberPasswordBox, &QCheckBox::stateChanged, [=](int) { - currentProfile().rememberPassword = rememberPasswordBox->isChecked(); - this->core.saveSettings(); - }); - loginLayout->addRow("Remember Password?", rememberPasswordBox); - - otpEdit = new QLineEdit(); - loginButton = new QPushButton("Login"); - registerButton = new QPushButton("Register"); - - connect(otpEdit, &QLineEdit::returnPressed, [this] { - if (loginButton->isEnabled()) - this->core.assetUpdater->update(currentProfile()); - }); - - connect(passwordEdit, &QLineEdit::returnPressed, [this] { - if (loginButton->isEnabled()) - this->core.assetUpdater->update(currentProfile()); - }); - - auto emptyWidget = new QWidget(); - emptyWidget->setLayout(layout); - setCentralWidget(emptyWidget); - - connect(core.assetUpdater, &AssetUpdater::finishedUpdating, [=] { - auto& profile = currentProfile(); - - auto info = new LoginInformation(); - info->settings = &profile; - info->username = usernameEdit->text(); - info->password = passwordEdit->text(); - info->oneTimePassword = otpEdit->text(); - - if (currentProfile().rememberUsername) { - profile.setKeychainValue("username", usernameEdit->text()); - } - - if (currentProfile().rememberPassword) { - profile.setKeychainValue("password", passwordEdit->text()); - } - - this->core.login(info); - }); - - connect(loginButton, &QPushButton::released, [=] { - // update the assets first if needed, then it calls the slot above :-) - this->core.assetUpdater->update(currentProfile()); - }); - - connect(registerButton, &QPushButton::released, [=] { - if (currentProfile().isSapphire) { - auto& profile = currentProfile(); - - LoginInformation info; - info.settings = &profile; - info.username = usernameEdit->text(); - info.password = passwordEdit->text(); - info.oneTimePassword = otpEdit->text(); - - this->core.sapphireLauncher->registerAccount(currentProfile().lobbyURL, info); - } - }); - - connect(&core, &LauncherCore::successfulLaunch, [&] { - if (core.appSettings.closeWhenLaunched) - hide(); - }); - - connect(&core, &LauncherCore::gameClosed, [&] { - if (core.appSettings.closeWhenLaunched) - QCoreApplication::quit(); - }); - - getHeadline(core, [&](Headline new_headline) { - this->headline = std::move(new_headline); - reloadNews(); - }); - - reloadControls(); -} - -ProfileSettings& LauncherWindow::currentProfile() { - return core.getProfile(profileSelect->currentIndex()); -} - -void LauncherWindow::reloadControls() { - if (currentlyReloadingControls) - return; - - currentlyReloadingControls = true; - - const int oldIndex = profileSelect->currentIndex(); - - profileSelect->clear(); - - for (const auto& profile : core.profileList()) { - profileSelect->addItem(profile); - } - - profileSelect->setCurrentIndex(oldIndex); - - if (profileSelect->currentIndex() == -1) { - profileSelect->setCurrentIndex(core.defaultProfileIndex); - } - - rememberUsernameBox->setChecked(currentProfile().rememberUsername); - if (currentProfile().rememberUsername) { - usernameEdit->setText(currentProfile().getKeychainValue("username")); - } - - rememberPasswordBox->setChecked(currentProfile().rememberPassword); - if (currentProfile().rememberPassword) { - passwordEdit->setText(currentProfile().getKeychainValue("password")); - } - - bool canLogin = true; - if (currentProfile().isSapphire) { - if (currentProfile().lobbyURL.isEmpty()) { - loginButton->setText("Login (Lobby URL is invalid)"); - canLogin = false; - } - } - -#if defined(Q_OS_LINUX) || defined(Q_OS_MAC) - if (!currentProfile().isWineInstalled() && !core.isSteam) { - loginButton->setText("Login (Wine is not installed)"); - canLogin = false; - } -#endif - if (!currentProfile().isGameInstalled()) { - loginButton->setText("Login (Game is not installed)"); - canLogin = false; - } - - if (canLogin) - loginButton->setText("Login"); - - launchOfficial->setEnabled(currentProfile().isGameInstalled()); - launchSysInfo->setEnabled(currentProfile().isGameInstalled()); - launchCfgBackup->setEnabled(currentProfile().isGameInstalled()); - - // Steam Deck's Game session has no file manager, so no point in having it here... - if(interface.isSteamDeck) { - openGameDir->setDisabled(true); - } else { - openGameDir->setEnabled(currentProfile().isGameInstalled()); - } - -#if defined(Q_OS_MAC) || defined(Q_OS_LINUX) - wineCfg->setEnabled(currentProfile().isWineInstalled()); -#endif - - layout->removeWidget(bannerScrollArea); - bannerScrollArea->hide(); - layout->removeWidget(newsListView); - newsListView->hide(); - - auto field = loginLayout->labelForField(otpEdit); - if (field != nullptr) - field->deleteLater(); - - loginLayout->takeRow(otpEdit); - otpEdit->hide(); - - if (currentProfile().useOneTimePassword && !currentProfile().isSapphire) { - loginLayout->addRow("One-Time Password", otpEdit); - otpEdit->show(); - } - - loginLayout->takeRow(loginButton); - loginButton->setEnabled(canLogin); - registerButton->setEnabled(canLogin); - loginLayout->addRow(loginButton); - - loginLayout->takeRow(registerButton); - registerButton->hide(); - - if (currentProfile().isSapphire) { - loginLayout->addRow(registerButton); - registerButton->show(); - } - - reloadNews(); - - currentlyReloadingControls = false; -} - -void LauncherWindow::reloadNews() { - if (core.appSettings.showBanners || core.appSettings.showNewsList) { - for (auto widget : bannerWidgets) { - bannerLayout->removeWidget(widget); - } - - bannerWidgets.clear(); - - int totalRow = 0; - if (core.appSettings.showBanners) { - bannerScrollArea->show(); - layout->addWidget(bannerScrollArea, totalRow++, 0); - } - - if (core.appSettings.showNewsList) { - newsListView->show(); - layout->addWidget(newsListView, totalRow++, 0); - } - - newsListView->clear(); - - if (!headline.banner.empty()) { - if (core.appSettings.showBanners) { - for (const auto& banner : headline.banner) { - auto request = QNetworkRequest(banner.bannerImage); - core.buildRequest(currentProfile(), request); - - auto reply = core.mgr->get(request); - connect(reply, &QNetworkReply::finished, [=] { - auto bannerImageView = new BannerWidget(); - bannerImageView->setUrl(banner.link); - - QPixmap pixmap; - pixmap.loadFromData(reply->readAll()); - bannerImageView->setPixmap(pixmap); - - bannerLayout->addWidget(bannerImageView); - bannerWidgets.push_back(bannerImageView); - }); - } - - if (bannerTimer == nullptr) { - bannerTimer = new QTimer(); - connect(bannerTimer, &QTimer::timeout, this, [=] { - if (currentBanner >= headline.banner.size()) - currentBanner = 0; - - bannerScrollArea->ensureVisible(640 * (currentBanner + 1), 0, 0, 0); - - currentBanner++; - }); - bannerTimer->start(5000); - } - } else { - if (bannerTimer != nullptr) { - bannerTimer->stop(); - bannerTimer->deleteLater(); - bannerTimer = nullptr; - } - } - - if (core.appSettings.showNewsList) { - auto newsItem = new QTreeWidgetItem((QTreeWidgetItem*)nullptr, QStringList("News")); - for (const auto& news : headline.news) { - auto item = new QTreeWidgetItem(); - item->setText(0, news.title); - item->setText(1, QLocale().toString(news.date, QLocale::ShortFormat)); - item->setData(0, Qt::UserRole, news.url); - - newsItem->addChild(item); - } - - auto pinnedItem = new QTreeWidgetItem((QTreeWidgetItem*)nullptr, QStringList("Pinned")); - for (const auto& pinned : headline.pinned) { - auto item = new QTreeWidgetItem(); - item->setText(0, pinned.title); - item->setText(1, QLocale().toString(pinned.date, QLocale::ShortFormat)); - item->setData(0, Qt::UserRole, pinned.url); - - pinnedItem->addChild(item); - } - - auto topicsItem = new QTreeWidgetItem((QTreeWidgetItem*)nullptr, QStringList("Topics")); - for (const auto& news : headline.topics) { - auto item = new QTreeWidgetItem(); - item->setText(0, news.title); - item->setText(1, QLocale().toString(news.date, QLocale::ShortFormat)); - item->setData(0, Qt::UserRole, news.url); - - topicsItem->addChild(item); - } - - newsListView->insertTopLevelItems(0, QList({newsItem, pinnedItem, topicsItem})); - - for (int i = 0; i < 3; i++) { - newsListView->expandItem(newsListView->topLevelItem(i)); - newsListView->resizeColumnToContents(i); - } - } - } - } -} - -void LauncherWindow::openPath(const QString& path) { -#if defined(Q_OS_WIN) - // for some reason, windows requires special treatment (what else is new?) - const QFileInfo fileInfo(path); - - QProcess::startDetached("explorer.exe", QStringList(QDir::toNativeSeparators(fileInfo.canonicalFilePath()))); -#else - QDesktopServices::openUrl("file://" + path); -#endif -} diff --git a/launcher/desktop/src/settingswindow.cpp b/launcher/desktop/src/settingswindow.cpp deleted file mode 100644 index b2addba..0000000 --- a/launcher/desktop/src/settingswindow.cpp +++ /dev/null @@ -1,694 +0,0 @@ -#include "settingswindow.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "gamescopesettingswindow.h" -#include "launchercore.h" -#include "launcherwindow.h" - -SettingsWindow::SettingsWindow(DesktopInterface& interface, int defaultTab, LauncherWindow& window, LauncherCore& core, QWidget* parent) - : core(core), window(window), interface(interface), VirtualDialog(interface, parent) { - setWindowTitle("Settings"); - setWindowModality(Qt::WindowModality::ApplicationModal); - - auto mainLayout = new QVBoxLayout(); - setLayout(mainLayout); - - auto tabWidget = new QTabWidget(); - mainLayout->addWidget(tabWidget); - - // general tab - { - auto generalTabWidget = new QWidget(); - tabWidget->addTab(generalTabWidget, "General"); - - auto layout = new QFormLayout(); - generalTabWidget->setLayout(layout); - - closeWhenLaunched = new QCheckBox("Close Astra when game is launched"); - connect(closeWhenLaunched, &QCheckBox::stateChanged, [&](int state) { - core.appSettings.closeWhenLaunched = state; - - core.saveSettings(); - }); - layout->addWidget(closeWhenLaunched); - - showBanner = new QCheckBox("Show news banners"); - connect(showBanner, &QCheckBox::stateChanged, [&](int state) { - core.appSettings.showBanners = state; - - core.saveSettings(); - window.reloadControls(); - }); - layout->addWidget(showBanner); - - showNewsList = new QCheckBox("Show news list"); - connect(showNewsList, &QCheckBox::stateChanged, [&](int state) { - core.appSettings.showNewsList = state; - - core.saveSettings(); - window.reloadControls(); - }); - layout->addWidget(showNewsList); - } - - // profile tab - { - auto profileTabWidget = new QWidget(); - tabWidget->addTab(profileTabWidget, "Profiles"); - - auto profileLayout = new QGridLayout(); - profileTabWidget->setLayout(profileLayout); - - auto profileTabs = new QTabWidget(); - profileLayout->addWidget(profileTabs, 1, 1, 3, 3); - - profileWidget = new QListWidget(); - profileWidget->addItem("INVALID *DEBUG*"); - profileWidget->setCurrentRow(0); - - connect(profileWidget, &QListWidget::currentRowChanged, this, &SettingsWindow::reloadControls); - - profileLayout->addWidget(profileWidget, 0, 0, 3, 1); - - auto addProfileButton = new QPushButton("Add Profile"); - connect(addProfileButton, &QPushButton::pressed, [=] { - profileWidget->setCurrentRow(this->core.addProfile()); - - this->core.saveSettings(); - }); - profileLayout->addWidget(addProfileButton, 3, 0); - - deleteAccountButton = new QPushButton("Delete Profile"); - connect(deleteAccountButton, &QPushButton::pressed, [=] { - profileWidget->setCurrentRow(this->core.deleteProfile(getCurrentProfile().name)); - - this->core.saveSettings(); - }); - profileLayout->addWidget(deleteAccountButton, 0, 2); - - nameEdit = new QLineEdit(); - connect(nameEdit, &QLineEdit::editingFinished, [=] { - getCurrentProfile().name = nameEdit->text(); - - reloadControls(); - this->core.saveSettings(); - }); - profileLayout->addWidget(nameEdit, 0, 1); - - // game options - { - auto gameTabWidget = new QWidget(); - profileTabs->addTab(gameTabWidget, "Game"); - - auto gameBoxLayout = new QFormLayout(); - gameTabWidget->setLayout(gameBoxLayout); - - setupGameTab(*gameBoxLayout); - } - - // login options - { - auto loginTabWidget = new QWidget(); - profileTabs->addTab(loginTabWidget, "Login"); - - auto loginBoxLayout = new QFormLayout(); - loginTabWidget->setLayout(loginBoxLayout); - - setupLoginTab(*loginBoxLayout); - } - -#if defined(Q_OS_MAC) || defined(Q_OS_LINUX) - // wine options - { - auto wineTabWidget = new QWidget(); - profileTabs->addTab(wineTabWidget, "Wine"); - - auto wineBoxLayout = new QFormLayout(); - wineTabWidget->setLayout(wineBoxLayout); - - setupWineTab(*wineBoxLayout); - } -#endif - - // dalamud options - { - auto dalamudTabWidget = new QWidget(); - profileTabs->addTab(dalamudTabWidget, "Dalamud"); - - auto dalamudBoxLayout = new QFormLayout(); - dalamudTabWidget->setLayout(dalamudBoxLayout); - - setupDalamudTab(*dalamudBoxLayout); - } - } - - { - auto accountsTabWidget = new QWidget(); - tabWidget->addTab(accountsTabWidget, "Accounts"); - - auto accountsLayout = new QGridLayout(); - accountsTabWidget->setLayout(accountsLayout); - - setupAccountsTab(*accountsLayout); - } - - tabWidget->setCurrentIndex(defaultTab); - - reloadControls(); -} - -void SettingsWindow::reloadControls() { - if (currentlyReloadingControls) - return; - - currentlyReloadingControls = true; - - auto oldRow = profileWidget->currentRow(); - - profileWidget->clear(); - - for (const auto& profile : core.profileList()) { - profileWidget->addItem(profile); - } - profileWidget->setCurrentRow(oldRow); - - closeWhenLaunched->setChecked(core.appSettings.closeWhenLaunched); - showBanner->setChecked(core.appSettings.showBanners); - showNewsList->setChecked(core.appSettings.showNewsList); - - // deleting the main profile is unsupported behavior - deleteAccountButton->setEnabled(profileWidget->currentRow() != 0); - - ProfileSettings& profile = core.getProfile(profileWidget->currentRow()); - nameEdit->setText(profile.name); - - // game - directXCombo->setCurrentIndex(profile.useDX9 ? 1 : 0); - currentGameDirectory->setText(profile.gamePath); - - if (!profile.isGameInstalled()) { - expansionVersionLabel->setText("No game installed."); - } else { - QString expacString; - - expacString += "Boot"; - expacString += QString(" (%1)\n").arg(profile.bootVersion); - - for (int i = 0; i < profile.repositories.repositories_count; i++) { - QString expansionName = "Unknown Expansion"; - if (i < core.expansionNames.size()) { - expansionName = core.expansionNames[i]; - } - - expacString += expansionName; - expacString += QString(" (%1)\n").arg(profile.repositories.repositories[i].version); - } - - expansionVersionLabel->setText(expacString); - } - - // wine -#if defined(Q_OS_LINUX) || defined(Q_OS_MAC) - if(!core.isSteam) { - if (!profile.isWineInstalled()) { - wineVersionLabel->setText("Wine is not installed."); - } else { - wineVersionLabel->setText(profile.wineVersion); - } - - wineTypeCombo->setCurrentIndex((int)profile.wineType); - selectWineButton->setEnabled(profile.wineType == WineType::Custom); - winePathLabel->setText(profile.winePath); - winePrefixDirectory->setText(profile.winePrefixPath); - } -#endif - -#if defined(Q_OS_LINUX) - useEsync->setChecked(profile.useEsync); - useGamescope->setChecked(profile.useGamescope); - useGamemode->setChecked(profile.useGamemode); - -#ifndef ENABLE_GAMEMODE - useGamemode->setEnabled(false); -#endif - - useGamemode->setEnabled(core.gamemodeAvailable); - useGamescope->setEnabled(core.gamescopeAvailable); - - configureGamescopeButton->setEnabled(profile.useGamescope); -#endif - -#ifdef ENABLE_WATCHDOG - enableWatchdog->setChecked(profile.enableWatchdog); -#endif - - // login - encryptArgumentsBox->setChecked(profile.encryptArguments); - serverType->setCurrentIndex(profile.isSapphire ? 1 : 0); - lobbyServerURL->setEnabled(profile.isSapphire); - if (profile.isSapphire) { - lobbyServerURL->setText(profile.lobbyURL); - lobbyServerURL->setPlaceholderText("Required..."); - } else { - lobbyServerURL->setText("neolobby0X.ffxiv.com"); - } - rememberUsernameBox->setChecked(profile.rememberUsername); - rememberPasswordBox->setChecked(profile.rememberPassword); - rememberOTPSecretBox->setChecked(profile.rememberOTPSecret); - rememberOTPSecretBox->setEnabled(profile.useOneTimePassword); - otpSecretButton->setEnabled(profile.rememberOTPSecret); - useOneTimePassword->setChecked(profile.useOneTimePassword); - useOneTimePassword->setEnabled(!profile.isSapphire); - if (!useOneTimePassword->isEnabled()) { - useOneTimePassword->setToolTip("OTP is not supported by Sapphire servers."); - } else { - useOneTimePassword->setToolTip(""); - } - autoLoginBox->setChecked(profile.autoLogin); - - gameLicenseBox->setCurrentIndex((int)profile.license); - gameLicenseBox->setEnabled(!profile.isSapphire); - if (!gameLicenseBox->isEnabled()) { - gameLicenseBox->setToolTip("Game licenses only matter when logging into the official Square Enix servers."); - } else { - gameLicenseBox->setToolTip(""); - } - - freeTrialBox->setChecked(profile.isFreeTrial); - - // dalamud - enableDalamudBox->setChecked(profile.dalamud.enabled); - if (core.dalamudVersion.isEmpty()) { - dalamudVersionLabel->setText("Dalamud is not installed."); - } else { - dalamudVersionLabel->setText(core.dalamudVersion); - } - - if (core.dalamudAssetVersion == -1) { - dalamudAssetVersionLabel->setText("Dalamud assets are not installed."); - } else { - dalamudAssetVersionLabel->setText(QString::number(core.dalamudAssetVersion)); - } - - dalamudOptOutBox->setChecked(profile.dalamud.optOutOfMbCollection); - dalamudChannel->setCurrentIndex((int)profile.dalamud.channel); - - window.reloadControls(); - - currentlyReloadingControls = false; -} - -ProfileSettings& SettingsWindow::getCurrentProfile() { - return this->core.getProfile(profileWidget->currentRow()); -} - -void SettingsWindow::setupGameTab(QFormLayout& layout) { - directXCombo = new QComboBox(); - directXCombo->addItem("DirectX 11"); - directXCombo->addItem("DirectX 9"); - layout.addRow("DirectX Version", directXCombo); - - connect(directXCombo, static_cast(&QComboBox::currentIndexChanged), [=](int index) { - getCurrentProfile().useDX9 = directXCombo->currentIndex() == 1; - this->core.saveSettings(); - }); - - currentGameDirectory = new QLabel(); - currentGameDirectory->setTextInteractionFlags(Qt::TextInteractionFlag::TextSelectableByMouse); - layout.addRow("Game Directory", currentGameDirectory); - - auto gameDirButtonLayout = new QHBoxLayout(); - auto gameDirButtonContainer = new QWidget(); - gameDirButtonContainer->setLayout(gameDirButtonLayout); - layout.addWidget(gameDirButtonContainer); - - auto selectDirectoryButton = new QPushButton("Select Game Directory"); - connect(selectDirectoryButton, &QPushButton::pressed, [this] { - getCurrentProfile().gamePath = QFileDialog::getExistingDirectory(nullptr, "Open Game Directory"); - - this->reloadControls(); - this->core.saveSettings(); - - this->core.readGameVersion(); - }); - gameDirButtonLayout->addWidget(selectDirectoryButton); - - gameDirectoryButton = new QPushButton("Open Game Directory"); - connect(gameDirectoryButton, &QPushButton::pressed, [this] { - window.openPath(getCurrentProfile().gamePath); - }); - gameDirButtonLayout->addWidget(gameDirectoryButton); - -#ifdef ENABLE_WATCHDOG - enableWatchdog = new QCheckBox("Enable Watchdog (X11 only)"); - layout.addWidget(enableWatchdog); - - connect(enableWatchdog, &QCheckBox::stateChanged, [this](int state) { - getCurrentProfile().enableWatchdog = state; - - this->core.saveSettings(); - }); -#endif - - gameDirectoryButton->setEnabled(getCurrentProfile().isGameInstalled()); - - expansionVersionLabel = new QLabel(); - expansionVersionLabel->setTextInteractionFlags(Qt::TextInteractionFlag::TextSelectableByMouse); - layout.addRow("Game Version", expansionVersionLabel); -} - -void SettingsWindow::setupLoginTab(QFormLayout& layout) { - encryptArgumentsBox = new QCheckBox(); - connect(encryptArgumentsBox, &QCheckBox::stateChanged, [=](int) { - getCurrentProfile().encryptArguments = encryptArgumentsBox->isChecked(); - - this->core.saveSettings(); - }); - layout.addRow("Encrypt Game Arguments", encryptArgumentsBox); - - serverType = new QComboBox(); - serverType->insertItem(0, "Square Enix"); - serverType->insertItem(1, "Sapphire"); - - connect(serverType, static_cast(&QComboBox::currentIndexChanged), [=](int index) { - getCurrentProfile().isSapphire = index == 1; - - reloadControls(); - this->core.saveSettings(); - }); - - layout.addRow("Server Lobby", serverType); - - lobbyServerURL = new QLineEdit(); - connect(lobbyServerURL, &QLineEdit::editingFinished, [=] { - getCurrentProfile().lobbyURL = lobbyServerURL->text(); - this->core.saveSettings(); - }); - layout.addRow("Lobby URL", lobbyServerURL); - - gameLicenseBox = new QComboBox(); - gameLicenseBox->insertItem(0, "Windows (Standalone)"); - gameLicenseBox->insertItem(1, "Windows (Steam)"); - gameLicenseBox->insertItem(2, "macOS"); - - connect(gameLicenseBox, static_cast(&QComboBox::currentIndexChanged), [=](int index) { - getCurrentProfile().license = (GameLicense)index; - - this->core.saveSettings(); - }); - - layout.addRow("Game License", gameLicenseBox); - - freeTrialBox = new QCheckBox(); - connect(freeTrialBox, &QCheckBox::stateChanged, [=](int) { - getCurrentProfile().isFreeTrial = freeTrialBox->isChecked(); - - this->core.saveSettings(); - }); - layout.addRow("Is Free Trial", freeTrialBox); - - rememberUsernameBox = new QCheckBox(); - connect(rememberUsernameBox, &QCheckBox::stateChanged, [=](int) { - getCurrentProfile().rememberUsername = rememberUsernameBox->isChecked(); - - this->core.saveSettings(); - }); - rememberUsernameBox->setToolTip("Relatively harmless option, can save your password for later for convince."); - layout.addRow("Remember Username", rememberUsernameBox); - - rememberPasswordBox = new QCheckBox(); - connect(rememberPasswordBox, &QCheckBox::stateChanged, [=](int) { - getCurrentProfile().rememberPassword = rememberPasswordBox->isChecked(); - - this->core.saveSettings(); - }); - rememberPasswordBox->setToolTip("You should only save your password when using OTP and you're fairly confident your system can keep it's keychain secure."); - layout.addRow("Remember Password", rememberPasswordBox); - - rememberOTPSecretBox = new QCheckBox(); - connect(rememberOTPSecretBox, &QCheckBox::stateChanged, [=](int) { - getCurrentProfile().rememberOTPSecret = rememberOTPSecretBox->isChecked(); - - this->core.saveSettings(); - this->reloadControls(); - }); - rememberOTPSecretBox->setToolTip("DANGEROUS! This should only be set if you're confident that your system keychain can securely store this. This trades convenience over the security an OTP can guarantee, so please be aware of that."); - layout.addRow("Remember OTP Secret", rememberOTPSecretBox); - - otpSecretButton = new QPushButton("Enter OTP Secret"); - connect(otpSecretButton, &QPushButton::pressed, [=] { - auto otpSecret = QInputDialog::getText(nullptr, "OTP Input", "Enter your OTP Secret:"); - - getCurrentProfile().setKeychainValue("otpsecret", otpSecret); - }); - otpSecretButton->setToolTip("Enter your OTP secret from Square Enix here. You cannot easily retrieve this if you forget it."); - layout.addRow(otpSecretButton); - - useOneTimePassword = new QCheckBox(); - connect(useOneTimePassword, &QCheckBox::stateChanged, [=](int) { - getCurrentProfile().useOneTimePassword = useOneTimePassword->isChecked(); - - this->core.saveSettings(); - this->window.reloadControls(); - this->reloadControls(); - }); - layout.addRow("Use One-Time Password", useOneTimePassword); - - autoLoginBox = new QCheckBox(); - connect(autoLoginBox, &QCheckBox::stateChanged, [=](int) { - getCurrentProfile().autoLogin = autoLoginBox->isChecked(); - - this->core.saveSettings(); - this->window.reloadControls(); - }); - layout.addRow("Auto-Login", autoLoginBox); -} - -void SettingsWindow::setupWineTab(QFormLayout& layout) { -#if defined(Q_OS_MAC) || defined(Q_OS_LINUX) - if(!core.isSteam) { - winePathLabel = new QLabel(); - winePathLabel->setTextInteractionFlags(Qt::TextInteractionFlag::TextSelectableByMouse); - layout.addRow("Wine Executable", winePathLabel); - - wineTypeCombo = new QComboBox(); - - #if defined(Q_OS_MAC) - wineTypeCombo->insertItem(2, "FFXIV for Mac (Official)"); - wineTypeCombo->insertItem(3, "XIV on Mac"); - #endif - - wineTypeCombo->insertItem(0, "System Wine"); - - // custom wine selection is broken under flatpak - #ifndef FLATPAK - wineTypeCombo->insertItem(1, "Custom Wine"); - #endif - - layout.addWidget(wineTypeCombo); - - selectWineButton = new QPushButton("Select Wine Executable"); - - #ifndef FLATPAK - layout.addWidget(selectWineButton); - #endif - - connect( - wineTypeCombo, static_cast(&QComboBox::currentIndexChanged), [this](int index) { - getCurrentProfile().wineType = (WineType)index; - - this->core.readWineInfo(getCurrentProfile()); - this->core.saveSettings(); - this->reloadControls(); - }); - - connect(selectWineButton, &QPushButton::pressed, [this] { - getCurrentProfile().winePath = QFileDialog::getOpenFileName(nullptr, "Open Wine Executable"); - - this->core.saveSettings(); - this->reloadControls(); - }); - - // wine version is reported incorrectly under flatpak too - wineVersionLabel = new QLabel(); - #ifndef FLATPAK - wineVersionLabel->setTextInteractionFlags(Qt::TextInteractionFlag::TextSelectableByMouse); - layout.addRow("Wine Version", wineVersionLabel); - #endif - - winePrefixDirectory = new QLabel(); - winePrefixDirectory->setTextInteractionFlags(Qt::TextInteractionFlag::TextSelectableByMouse); - layout.addRow("Wine Prefix", winePrefixDirectory); - - auto winePrefixButtonLayout = new QHBoxLayout(); - auto winePrefixButtonContainer = new QWidget(); - winePrefixButtonContainer->setLayout(winePrefixButtonLayout); - layout.addWidget(winePrefixButtonContainer); - - auto selectPrefixButton = new QPushButton("Select Wine Prefix"); - connect(selectPrefixButton, &QPushButton::pressed, [this] { - getCurrentProfile().winePrefixPath = QFileDialog::getExistingDirectory(nullptr, "Open Wine Prefix"); - - this->core.saveSettings(); - this->reloadControls(); - }); - winePrefixButtonLayout->addWidget(selectPrefixButton); - - auto openPrefixButton = new QPushButton("Open Wine Prefix"); - connect(openPrefixButton, &QPushButton::pressed, [this] { - window.openPath(getCurrentProfile().winePrefixPath); - }); - winePrefixButtonLayout->addWidget(openPrefixButton); - } else { - auto label = new QLabel("You are launching Astra via Steam. Proton is used automatically and can not be configured."); - layout.addWidget(label); - } - - auto enableDXVKhud = new QCheckBox("Enable DXVK HUD"); - layout.addRow("Wine Tweaks", enableDXVKhud); - - connect(enableDXVKhud, &QCheckBox::stateChanged, [this](int state) { - getCurrentProfile().enableDXVKhud = state; - this->core.settings.setValue("enableDXVKhud", static_cast(state)); - }); -#endif - -#if defined(Q_OS_LINUX) - useEsync = new QCheckBox("Use Better Sync Primitives (Esync, Fsync, and Futex2)"); - layout.addWidget(useEsync); - - useEsync->setToolTip( - "This may improve game performance, but requires a Wine and kernel with the patches included."); - - connect(useEsync, &QCheckBox::stateChanged, [this](int state) { - getCurrentProfile().useEsync = state; - - this->core.saveSettings(); - }); - - useGamescope = new QCheckBox("Use Gamescope"); - layout.addWidget(useGamescope); - - useGamescope->setToolTip( - "Use the micro-compositor compositor that uses Wayland and XWayland to create a nested session.\nIf you " - "primarily use fullscreen mode, this may improve input handling especially on Wayland."); - - auto gamescopeButtonLayout = new QHBoxLayout(); - auto gamescopeButtonContainer = new QWidget(); - gamescopeButtonContainer->setLayout(gamescopeButtonLayout); - layout.addWidget(gamescopeButtonContainer); - - configureGamescopeButton = new QPushButton("Configure..."); - connect(configureGamescopeButton, &QPushButton::pressed, [&] { - auto gamescopeSettingsWindow = new GamescopeSettingsWindow(interface, getCurrentProfile(), this->core, this->getRootWidget()); - gamescopeSettingsWindow->show(); - }); - gamescopeButtonLayout->addWidget(configureGamescopeButton); - - connect(useGamescope, &QCheckBox::stateChanged, [this](int state) { - getCurrentProfile().useGamescope = state; - - this->core.saveSettings(); - this->reloadControls(); - }); - - useGamemode = new QCheckBox("Use GameMode"); - layout.addWidget(useGamemode); - - useGamemode->setToolTip("A special game performance enhancer, which automatically tunes your CPU scheduler among " - "other things. This may improve game performance."); - - connect(useGamemode, &QCheckBox::stateChanged, [this](int state) { - getCurrentProfile().useGamemode = state; - - this->core.saveSettings(); - }); -#endif -} - -void SettingsWindow::setupDalamudTab(QFormLayout& layout) { - enableDalamudBox = new QCheckBox(); - connect(enableDalamudBox, &QCheckBox::stateChanged, [=](int) { - getCurrentProfile().dalamud.enabled = enableDalamudBox->isChecked(); - - this->core.saveSettings(); - }); - layout.addRow("Enable Dalamud Plugins", enableDalamudBox); - - dalamudOptOutBox = new QCheckBox(); - connect(dalamudOptOutBox, &QCheckBox::stateChanged, [=](int) { - getCurrentProfile().dalamud.optOutOfMbCollection = dalamudOptOutBox->isChecked(); - - this->core.saveSettings(); - }); - layout.addRow("Opt Out of Automatic Marketboard Collection", dalamudOptOutBox); - - dalamudChannel = new QComboBox(); - dalamudChannel->insertItem(0, "Stable"); - dalamudChannel->insertItem(1, "Staging"); - dalamudChannel->insertItem(2, ".NET 5"); - - connect(dalamudChannel, static_cast(&QComboBox::currentIndexChanged), [=](int index) { - getCurrentProfile().dalamud.channel = (DalamudChannel)index; - - this->core.saveSettings(); - }); - - layout.addRow("Dalamud Update Channel", dalamudChannel); - - dalamudVersionLabel = new QLabel(); - dalamudVersionLabel->setTextInteractionFlags(Qt::TextInteractionFlag::TextSelectableByMouse); - layout.addRow("Dalamud Version", dalamudVersionLabel); - - dalamudAssetVersionLabel = new QLabel(); - dalamudAssetVersionLabel->setTextInteractionFlags(Qt::TextInteractionFlag::TextSelectableByMouse); - layout.addRow("Dalamud Asset Version", dalamudAssetVersionLabel); -} - -void SettingsWindow::setupAccountsTab(QGridLayout& layout) { - auto profileTabs = new QTabWidget(); - layout.addWidget(profileTabs, 1, 1, 3, 3); - - accountWidget = new QListWidget(); - accountWidget->addItem("INVALID *DEBUG*"); - accountWidget->setCurrentRow(0); - - connect(accountWidget, &QListWidget::currentRowChanged, this, &SettingsWindow::reloadControls); - - layout.addWidget(accountWidget, 0, 0, 3, 1); - - auto addAccountButton = new QPushButton("Add Account"); - connect(addAccountButton, &QPushButton::pressed, [=] { - accountWidget->setCurrentRow(this->core.addProfile()); - - this->core.saveSettings(); - }); - layout.addWidget(addAccountButton, 3, 0); - - deleteAccountButton = new QPushButton("Remove Account"); - connect(deleteAccountButton, &QPushButton::pressed, [=] { - accountWidget->setCurrentRow(this->core.deleteProfile(getCurrentProfile().name)); - - this->core.saveSettings(); - }); - layout.addWidget(deleteAccountButton, 0, 2); - - nameEdit = new QLineEdit(); - connect(nameEdit, &QLineEdit::editingFinished, [=] { - //getCurrentProfile().name = nameEdit->text(); - - reloadControls(); - this->core.saveSettings(); - }); - layout.addWidget(nameEdit, 0, 1); -} \ No newline at end of file diff --git a/launcher/desktop/src/virtualdialog.cpp b/launcher/desktop/src/virtualdialog.cpp deleted file mode 100644 index 3861a38..0000000 --- a/launcher/desktop/src/virtualdialog.cpp +++ /dev/null @@ -1,75 +0,0 @@ -#include "virtualdialog.h" - -#include - -#include "desktopinterface.h" - -VirtualDialog::VirtualDialog(DesktopInterface& interface, QWidget* widget) : interface(interface), QObject(widget) { - if (interface.oneWindow) { - mdi_window = new QMdiSubWindow(); - mdi_window->setAttribute(Qt::WA_DeleteOnClose); - } else { - normal_dialog = new QDialog(); - } - - interface.addDialog(this); -} - -void VirtualDialog::setWindowTitle(const QString& title) { - if (interface.oneWindow) { - mdi_window->setWindowTitle(title); - } else { - normal_dialog->setWindowTitle(title); - } -} - -void VirtualDialog::show() { - if (interface.oneWindow) { - mdi_window->show(); - } else { - normal_dialog->show(); - } -} - -void VirtualDialog::hide() { - if(interface.oneWindow) { - mdi_window->hide(); - } else { - normal_dialog->hide(); - } -} - -void VirtualDialog::close() { - if(interface.oneWindow) { - mdi_window->close(); - } else { - normal_dialog->close(); - } -} - -void VirtualDialog::setWindowModality(Qt::WindowModality modality) { - if(interface.oneWindow) { - mdi_window->setWindowModality(modality); - } else { - normal_dialog->setWindowModality(modality); - } -} - -void VirtualDialog::setLayout(QLayout* layout) { - if(interface.oneWindow) { - auto emptyWidget = new QWidget(); - emptyWidget->setLayout(layout); - - mdi_window->layout()->addWidget(emptyWidget); - } else { - normal_dialog->setLayout(layout); - } -} - -QWidget* VirtualDialog::getRootWidget() { - if(interface.oneWindow) { - return mdi_window; - } else { - return normal_dialog; - } -} diff --git a/launcher/desktop/src/virtualwindow.cpp b/launcher/desktop/src/virtualwindow.cpp deleted file mode 100644 index 38cad98..0000000 --- a/launcher/desktop/src/virtualwindow.cpp +++ /dev/null @@ -1,77 +0,0 @@ -#include "virtualwindow.h" - -#include -#include - -#include "desktopinterface.h" - -VirtualWindow::VirtualWindow(DesktopInterface& interface, QWidget* widget) : interface(interface), QObject(widget) { - if (interface.oneWindow) { - mdi_window = new QMdiSubWindow(); - mdi_window->setAttribute(Qt::WA_DeleteOnClose); - } else { - normal_window = new QMainWindow(); - } - - interface.addWindow(this); -} - -void VirtualWindow::setWindowTitle(const QString& title) { - if (interface.oneWindow) { - mdi_window->setWindowTitle(title); - } else { - normal_window->setWindowTitle(title); - } -} - -void VirtualWindow::show() { - if(interface.oneWindow) { - mdi_window->show(); - } else { - normal_window->show(); - } -} - -void VirtualWindow::setCentralWidget(QWidget* widget) { - if(interface.oneWindow) { - mdi_window->layout()->addWidget(widget); - } else { - normal_window->setCentralWidget(widget); - } -} - -void VirtualWindow::hide() { - if(interface.oneWindow) { - mdi_window->hide(); - } else { - normal_window->hide(); - } -} - -QMenuBar* VirtualWindow::menuBar() { - if(interface.oneWindow) { - if(mdi_window->layout()->menuBar() == nullptr) { - mdi_window->layout()->setMenuBar(new QMenuBar()); - } - - return dynamic_cast(mdi_window->layout()->menuBar()); - } else { - return normal_window->menuBar(); - } -} - -void VirtualWindow::showMaximized() { - if(interface.oneWindow) { - mdi_window->showMaximized(); - } else { - normal_window->showMaximized(); - } -} - -QWidget* VirtualWindow::getRootWidget() { - if(interface.oneWindow) { - return mdi_window; - } else { - return normal_window; - } -} diff --git a/launcher/include/account.h b/launcher/include/account.h new file mode 100644 index 0000000..57c8d07 --- /dev/null +++ b/launcher/include/account.h @@ -0,0 +1,95 @@ +#pragma once + +#include + +#include "accountconfig.h" + +class LauncherCore; + +class Account : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) + Q_PROPERTY(QString lodestoneId READ lodestoneId WRITE setLodestoneId NOTIFY lodestoneIdChanged) + Q_PROPERTY(QString avatarUrl READ avatarUrl NOTIFY avatarUrlChanged) + Q_PROPERTY(bool isSapphire READ isSapphire WRITE setIsSapphire NOTIFY isSapphireChanged) + Q_PROPERTY(QString lobbyUrl READ lobbyUrl WRITE setLobbyUrl NOTIFY lobbyUrlChanged) + Q_PROPERTY(bool rememberPassword READ rememberPassword WRITE setRememberPassword NOTIFY rememberPasswordChanged) + Q_PROPERTY(bool rememberOTP READ rememberOTP WRITE setRememberOTP NOTIFY rememberOTPChanged) + Q_PROPERTY(bool useOTP READ useOTP WRITE setUseOTP NOTIFY useOTPChanged) + Q_PROPERTY(GameLicense license READ license WRITE setLicense NOTIFY licenseChanged) + Q_PROPERTY(bool isFreeTrial READ isFreeTrial WRITE setIsFreeTrial NOTIFY isFreeTrialChanged) + +public: + explicit Account(LauncherCore &launcher, const QString &key, QObject *parent = nullptr); + + enum class GameLicense { WindowsStandalone, WindowsSteam, macOS }; + Q_ENUM(GameLicense) + + QString uuid() const; + + QString name() const; + void setName(const QString &name); + + QString lodestoneId() const; + void setLodestoneId(const QString &id); + + QString avatarUrl() const; + + bool isSapphire() const; + void setIsSapphire(bool value); + + QString lobbyUrl() const; + void setLobbyUrl(const QString &url); + + bool rememberPassword() const; + void setRememberPassword(bool value); + + bool rememberOTP() const; + void setRememberOTP(bool value); + + bool useOTP() const; + void setUseOTP(bool value); + + GameLicense license() const; + void setLicense(GameLicense license); + + bool isFreeTrial() const; + void setIsFreeTrial(bool value); + + Q_INVOKABLE QString getPassword() const; + void setPassword(const QString &password); + + Q_INVOKABLE QString getOTP() const; + +Q_SIGNALS: + void nameChanged(); + void lodestoneIdChanged(); + void avatarUrlChanged(); + void isSapphireChanged(); + void lobbyUrlChanged(); + void rememberPasswordChanged(); + void rememberOTPChanged(); + void useOTPChanged(); + void licenseChanged(); + void isFreeTrialChanged(); + +private: + void fetchAvatar(); + + /* + * Sets a value in the keychain. This function is asynchronous. + */ + void setKeychainValue(const QString &key, const QString &value) const; + + /* + * Retrieves a value from the keychain. This function is synchronous. + */ + QString getKeychainValue(const QString &key) const; + + AccountConfig m_config; + QString m_key; + QUrl m_url; + LauncherCore &m_launcher; +}; \ No newline at end of file diff --git a/launcher/include/accountmanager.h b/launcher/include/accountmanager.h new file mode 100644 index 0000000..bbee9f0 --- /dev/null +++ b/launcher/include/accountmanager.h @@ -0,0 +1,42 @@ +#pragma once + +#include + +#include "account.h" + +class AccountManager : public QAbstractListModel +{ + Q_OBJECT + +public: + explicit AccountManager(LauncherCore &launcher, QObject *parent = nullptr); + + void load(); + + enum CustomRoles { + AccountRole = Qt::UserRole, + }; + + int rowCount(const QModelIndex &index = QModelIndex()) const override; + + QVariant data(const QModelIndex &index, int role) const override; + + QHash roleNames() const override; + + Q_INVOKABLE Account *createSquareEnixAccount(const QString &username, int licenseType, bool isFreeTrial); + Q_INVOKABLE Account *createSapphireAccount(const QString &lobbyUrl, const QString &username); + + Account *getByUuid(const QString &uuid) const; + + Q_INVOKABLE bool canDelete(Account *account) const; + Q_INVOKABLE void deleteAccount(Account *account); + +Q_SIGNALS: + +private: + void insertAccount(Account *account); + + QVector m_accounts; + + LauncherCore &m_launcher; +}; \ No newline at end of file diff --git a/launcher/core/include/assetupdater.h b/launcher/include/assetupdater.h similarity index 74% rename from launcher/core/include/assetupdater.h rename to launcher/include/assetupdater.h index e9721c6..9b21f52 100644 --- a/launcher/core/include/assetupdater.h +++ b/launcher/include/assetupdater.h @@ -2,21 +2,20 @@ #include #include -#include #include #include "launchercore.h" class LauncherCore; class QNetworkReply; -struct ProfileSettings; -class AssetUpdater : public QObject { +class AssetUpdater : public QObject +{ Q_OBJECT public: - explicit AssetUpdater(LauncherCore& launcher); + explicit AssetUpdater(Profile &profile, LauncherCore &launcher, QObject *parent = nullptr); - void update(const ProfileSettings& profile); + void update(); void beginInstall(); void checkIfCheckingIsDone(); @@ -27,11 +26,9 @@ signals: void finishedUpdating(); private: - LauncherCore& launcher; + LauncherCore &launcher; - QProgressDialog* dialog; - - DalamudChannel chosenChannel; + Profile::DalamudChannel chosenChannel; QString remoteDalamudVersion; QString remoteRuntimeVersion; @@ -49,4 +46,5 @@ private: QJsonArray remoteDalamudAssetArray; QString dataDir; + Profile &m_profile; }; diff --git a/launcher/include/encryptedarg.h b/launcher/include/encryptedarg.h new file mode 100644 index 0000000..9e73592 --- /dev/null +++ b/launcher/include/encryptedarg.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +QString encryptGameArg(const QString &arg); \ No newline at end of file diff --git a/launcher/include/gameinstaller.h b/launcher/include/gameinstaller.h new file mode 100644 index 0000000..cb723a9 --- /dev/null +++ b/launcher/include/gameinstaller.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +class LauncherCore; +class Profile; + +class GameInstaller : public QObject +{ + Q_OBJECT +public: + GameInstaller(LauncherCore &launcher, Profile &profile, QObject *parent = nullptr); + + Q_INVOKABLE void installGame(); + +Q_SIGNALS: + void installFinished(); + +private: + LauncherCore &m_launcher; + Profile &m_profile; +}; \ No newline at end of file diff --git a/launcher/core/include/gameparser.h b/launcher/include/gameparser.h similarity index 70% rename from launcher/core/include/gameparser.h rename to launcher/include/gameparser.h index 5bfc7e1..d46f00c 100644 --- a/launcher/core/include/gameparser.h +++ b/launcher/include/gameparser.h @@ -4,14 +4,7 @@ #include #include -enum class ScreenState { - Splash, - LobbyError, - WorldFull, - ConnectingToDataCenter, - EnteredTitleScreen, - InLoginQueue -}; +enum class ScreenState { Splash, LobbyError, WorldFull, ConnectingToDataCenter, EnteredTitleScreen, InLoginQueue }; struct GameParseResult { ScreenState state; @@ -19,15 +12,18 @@ struct GameParseResult { int playersInQueue = -1; }; -inline bool operator==(const GameParseResult a, const GameParseResult b) { +inline bool operator==(const GameParseResult a, const GameParseResult b) +{ return a.state == b.state && a.playersInQueue == b.playersInQueue; } -inline bool operator!=(const GameParseResult a, const GameParseResult b) { +inline bool operator!=(const GameParseResult a, const GameParseResult b) +{ return !(a == b); } -class GameParser { +class GameParser +{ public: GameParser(); ~GameParser(); @@ -35,5 +31,5 @@ public: GameParseResult parseImage(QImage image); private: - tesseract::TessBaseAPI* api; + tesseract::TessBaseAPI *api; }; \ No newline at end of file diff --git a/launcher/include/headline.h b/launcher/include/headline.h new file mode 100644 index 0000000..c56090b --- /dev/null +++ b/launcher/include/headline.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include + +class News +{ + Q_GADGET + + Q_PROPERTY(QDateTime date MEMBER date CONSTANT) + Q_PROPERTY(QString id MEMBER id CONSTANT) + Q_PROPERTY(QString tag MEMBER tag CONSTANT) + Q_PROPERTY(QString title MEMBER title CONSTANT) + Q_PROPERTY(QUrl url MEMBER url CONSTANT) + +public: + QDateTime date; + QString id; + QString tag; + QString title; + QUrl url; +}; + +class Banner +{ + Q_GADGET + + Q_PROPERTY(QUrl link MEMBER link CONSTANT) + Q_PROPERTY(QUrl bannerImage MEMBER bannerImage CONSTANT) + +public: + QUrl link; + QUrl bannerImage; +}; + +class Headline : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QList banners MEMBER banners CONSTANT) + Q_PROPERTY(QList news MEMBER news CONSTANT) + Q_PROPERTY(QList pinned MEMBER pinned CONSTANT) + Q_PROPERTY(QList topics MEMBER topics CONSTANT) + +public: + explicit Headline(QObject *parent = nullptr) + : QObject(parent) + { + } + + QList banners; + QList news; + QList pinned; + QList topics; +}; \ No newline at end of file diff --git a/launcher/include/launchercore.h b/launcher/include/launchercore.h new file mode 100755 index 0000000..af27af4 --- /dev/null +++ b/launcher/include/launchercore.h @@ -0,0 +1,182 @@ +#pragma once + +#include +#include +#include +#include + +#include "accountmanager.h" +#include "headline.h" +#include "profile.h" +#include "profilemanager.h" +#include "squareboot.h" +#include "steamapi.h" + +class SapphireLauncher; +class SquareLauncher; +class AssetUpdater; +class Watchdog; +class GameInstaller; + +class LoginInformation : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QString username MEMBER username) + Q_PROPERTY(QString password MEMBER password) + Q_PROPERTY(QString oneTimePassword MEMBER oneTimePassword) + Q_PROPERTY(Profile *profile MEMBER profile) + +public: + Profile *profile = nullptr; + + QString username, password, oneTimePassword; +}; + +struct LoginAuth { + QString SID; + int region = 2; // america? + int maxExpansion = 1; + + // if empty, dont set on the client + QString lobbyhost, frontierHost; +}; + +class LauncherCore : public QObject +{ + Q_OBJECT + + Q_PROPERTY(bool loadingFinished READ isLoadingFinished NOTIFY loadingFinished) + Q_PROPERTY(bool hasAccount READ hasAccount NOTIFY accountChanged) + Q_PROPERTY(bool isSteam READ isSteam CONSTANT) + Q_PROPERTY(SquareBoot *squareBoot MEMBER squareBoot) + Q_PROPERTY(ProfileManager *profileManager READ profileManager CONSTANT) + Q_PROPERTY(AccountManager *accountManager READ accountManager CONSTANT) + Q_PROPERTY(bool closeWhenLaunched READ closeWhenLaunched WRITE setCloseWhenLaunched NOTIFY closeWhenLaunchedChanged) + Q_PROPERTY(bool showNewsBanners READ showNewsBanners WRITE setShowNewsBanners NOTIFY showNewsBannersChanged) + Q_PROPERTY(bool showNewsList READ showNewsList WRITE setShowNewsList NOTIFY showNewsListChanged) + Q_PROPERTY(Headline *headline READ headline NOTIFY newsChanged) + +public: + explicit LauncherCore(bool isSteam); + + QNetworkAccessManager *mgr; + + ProfileManager *profileManager(); + AccountManager *accountManager(); + + /* + * Begins the login process, and may call SquareBoot or SapphireLauncher depending on the profile type. + * It's designed to be opaque as possible to the caller. + * + * The login process is asynchronous. + */ + Q_INVOKABLE void login(Profile *profile, const QString &username, const QString &password, const QString &oneTimePassword); + + /* + * Attempts to log into a profile without LoginInformation, which may or may not work depending on a combination of + * the password failing, OTP not being available to auto-generate, among other things. + * + * The launcher will still warn the user about any possible errors, however the call site will need to check the + * result to see whether they need to "reset" or show a failed state or not. + */ + bool autoLogin(Profile &settings); + + /* + * Launches the game using the provided authentication. + */ + void launchGame(const Profile &settings, const LoginAuth &auth); + + /* + * This just wraps it in wine if needed. + */ + void launchExecutable(const Profile &settings, QProcess *process, const QStringList &args, bool isGame, bool needsRegistrySetup); + + void addRegistryKey(const Profile &settings, QString key, QString value, QString data); + + void buildRequest(const Profile &settings, QNetworkRequest &request); + void setSSL(QNetworkRequest &request); + void readInitialInformation(); + + SapphireLauncher *sapphireLauncher; + SquareBoot *squareBoot; + SquareLauncher *squareLauncher; + AssetUpdater *assetUpdater; + Watchdog *watchdog; + + bool gamescopeAvailable = false; + bool gamemodeAvailable = false; + + bool closeWhenLaunched() const; + void setCloseWhenLaunched(bool value); + + bool showNewsBanners() const; + void setShowNewsBanners(bool value); + + bool showNewsList() const; + void setShowNewsList(bool value); + + int defaultProfileIndex = 0; + + bool m_isSteam = false; + + Q_INVOKABLE GameInstaller *createInstaller(Profile &profile); + + bool isLoadingFinished() const; + bool hasAccount() const; + bool isSteam() const; + + Q_INVOKABLE void refreshNews(); + Headline *headline(); + + Q_INVOKABLE void openOfficialLauncher(Profile *profile); + Q_INVOKABLE void openSystemInfo(Profile *profile); + Q_INVOKABLE void openConfigBackup(Profile *profile); + +signals: + void loadingFinished(); + void gameInstallationChanged(); + void accountChanged(); + void settingsChanged(); + void successfulLaunch(); + void gameClosed(); + void closeWhenLaunchedChanged(); + void showNewsBannersChanged(); + void showNewsListChanged(); + void loginError(QString message); + void stageChanged(QString message); + void newsChanged(); + +private: + /* + * Begins the game executable, but calls to Dalamud if needed. + */ + void beginGameExecutable(const Profile &settings, const LoginAuth &auth); + + /* + * Starts a vanilla game session with no Dalamud injection. + */ + void beginVanillaGame(const QString &gameExecutablePath, const Profile &profile, const LoginAuth &auth); + + /* + * Starts a game session with Dalamud injected. + */ + void beginDalamudGame(const QString &gameExecutablePath, const Profile &profile, const LoginAuth &auth); + + /* + * Returns the game arguments needed to properly launch the game. This encrypts it too if needed, and it's already + * joined! + */ + QString getGameArgs(const Profile &profile, const LoginAuth &auth); + + bool checkIfInPath(const QString &program); + + SteamAPI *steamApi = nullptr; + + bool m_loadingFinished = false; + + ProfileManager *m_profileManager = nullptr; + AccountManager *m_accountManager = nullptr; + + Headline *m_headline = nullptr; +}; diff --git a/launcher/core/include/patcher.h b/launcher/include/patcher.h similarity index 52% rename from launcher/core/include/patcher.h rename to launcher/include/patcher.h index d48473c..962b52e 100644 --- a/launcher/core/include/patcher.h +++ b/launcher/include/patcher.h @@ -1,19 +1,19 @@ #pragma once #include -#include #include #include // General-purpose patcher routine. It opens a nice dialog box, handles downloading // and processing patches. -class Patcher : public QObject { +class Patcher : public QObject +{ Q_OBJECT public: - Patcher(QString baseDirectory, GameData* game_data); - Patcher(QString baseDirectory, BootData* game_data); + Patcher(QString baseDirectory, GameData *game_data, QObject *parent = nullptr); + Patcher(QString baseDirectory, BootData *game_data, QObject *parent = nullptr); - void processPatchList(QNetworkAccessManager& mgr, const QString& patchList); + void processPatchList(QNetworkAccessManager &mgr, const QString &patchList); signals: void done(); @@ -21,7 +21,8 @@ signals: private: void checkIfDone(); - [[nodiscard]] bool isBoot() const { + [[nodiscard]] bool isBoot() const + { return boot_data != nullptr; } @@ -29,15 +30,13 @@ private: QString name, repository, version, path; }; - void processPatch(const QueuedPatch& patch); + void processPatch(const QueuedPatch &patch); QVector patchQueue; QString baseDirectory; - BootData* boot_data = nullptr; - GameData* game_data = nullptr; - - QProgressDialog* dialog = nullptr; + BootData *boot_data = nullptr; + GameData *game_data = nullptr; int remainingPatches = -1; }; \ No newline at end of file diff --git a/launcher/include/profile.h b/launcher/include/profile.h new file mode 100644 index 0000000..569d31a --- /dev/null +++ b/launcher/include/profile.h @@ -0,0 +1,181 @@ +#pragma once + +#include + +#include "profileconfig.h" +#include "squareboot.h" + +class Account; + +class Profile : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) + Q_PROPERTY(int language READ language WRITE setLanguage NOTIFY languageChanged) + Q_PROPERTY(QString gamePath READ gamePath WRITE setGamePath NOTIFY gamePathChanged) + Q_PROPERTY(QString winePath READ winePath WRITE setWinePath NOTIFY winePathChanged) + Q_PROPERTY(QString winePrefixPath READ winePrefixPath WRITE setWinePrefixPath NOTIFY winePrefixPathChanged) + Q_PROPERTY(bool watchdogEnabled READ watchdogEnabled WRITE setWatchdogEnabled NOTIFY enableWatchdogChanged) + Q_PROPERTY(WineType wineType READ wineType WRITE setWineType NOTIFY wineTypeChanged) + Q_PROPERTY(bool esyncEnabled READ esyncEnabled WRITE setESyncEnabled NOTIFY useESyncChanged) + Q_PROPERTY(bool gamescopeEnabled READ gamescopeEnabled WRITE setGamescopeEnabled NOTIFY useGamescopeChanged) + Q_PROPERTY(bool gamemodeEnabled READ gamemodeEnabled WRITE setGamemodeEnabled NOTIFY useGamemodeChanged) + Q_PROPERTY(bool directx9Enabled READ directx9Enabled WRITE setDirectX9Enabled NOTIFY useDX9Changed) + Q_PROPERTY(bool gamescopeFullscreen READ gamescopeFullscreen WRITE setGamescopeFullscreen NOTIFY gamescopeFullscreenChanged) + Q_PROPERTY(bool gamescopeBorderless READ gamescopeBorderless WRITE setGamescopeBorderless NOTIFY gamescopeBorderlessChanged) + Q_PROPERTY(int gamescopeWidth READ gamescopeWidth WRITE setGamescopeWidth NOTIFY gamescopeWidthChanged) + Q_PROPERTY(int gamescopeHeight READ gamescopeHeight WRITE setGamescopeHeight NOTIFY gamescopeHeightChanged) + Q_PROPERTY(int gamescopeRefreshRate READ gamescopeRefreshRate WRITE setGamescopeRefreshRate NOTIFY gamescopeRefreshRateChanged) + Q_PROPERTY(bool dalamudEnabled READ dalamudEnabled WRITE setDalamudEnabled NOTIFY dalamudEnabledChanged) + Q_PROPERTY(bool dalamudOptOut READ dalamudOptOut WRITE setDalamudOptOut NOTIFY dalamudOptOutChanged) + Q_PROPERTY(DalamudChannel dalamudChannel READ dalamudChannel WRITE setDalamudChannel NOTIFY dalamudChannelChanged) + Q_PROPERTY(bool argumentsEncrypted READ argumentsEncrypted WRITE setArgumentsEncrypted NOTIFY encryptedArgumentsChanged) + Q_PROPERTY(bool isGameInstalled READ isGameInstalled NOTIFY gameInstallChanged) + Q_PROPERTY(Account *account READ account WRITE setAccount NOTIFY accountChanged) + Q_PROPERTY(QString expansionVersionText READ expansionVersionText NOTIFY gameInstallChanged) + Q_PROPERTY(QString dalamudVersionText READ dalamudVersionText NOTIFY gameInstallChanged) + Q_PROPERTY(QString wineVersionText READ wineVersionText NOTIFY wineChanged) + +public: + explicit Profile(LauncherCore &launcher, const QString &key, QObject *parent = nullptr); + + enum class WineType { + System, + Custom, + Builtin, // macos only + XIVOnMac // macos only + }; + Q_ENUM(WineType) + + enum class DalamudChannel { Stable, Staging, Net5 }; + Q_ENUM(DalamudChannel) + + QString uuid() const; + + QString name() const; + void setName(const QString &name); + + int language() const; + void setLanguage(int value); + + QString gamePath() const; + void setGamePath(const QString &path); + + QString winePath() const; + void setWinePath(const QString &path); + + QString winePrefixPath() const; + void setWinePrefixPath(const QString &path); + + bool watchdogEnabled() const; + void setWatchdogEnabled(bool value); + + WineType wineType() const; + void setWineType(WineType type); + + bool esyncEnabled() const; + void setESyncEnabled(bool value); + + bool gamescopeEnabled() const; + void setGamescopeEnabled(bool value); + + bool gamemodeEnabled() const; + void setGamemodeEnabled(bool value); + + bool directx9Enabled() const; + void setDirectX9Enabled(bool value); + + bool gamescopeFullscreen() const; + void setGamescopeFullscreen(bool value); + + bool gamescopeBorderless() const; + void setGamescopeBorderless(bool value); + + int gamescopeWidth() const; + void setGamescopeWidth(int value); + + int gamescopeHeight() const; + void setGamescopeHeight(int value); + + int gamescopeRefreshRate() const; + void setGamescopeRefreshRate(int value); + + bool dalamudEnabled() const; + void setDalamudEnabled(bool value); + + bool dalamudOptOut() const; + void setDalamudOptOut(bool value); + + DalamudChannel dalamudChannel() const; + void setDalamudChannel(DalamudChannel channel); + + bool argumentsEncrypted() const; + void setArgumentsEncrypted(bool value); + + Account *account() const; + QString accountUuid() const; + void setAccount(Account *account); + + void readGameData(); + void readGameVersion(); + void readWineInfo(); + + QVector expansionNames; + + BootData *bootData; + GameData *gameData; + + physis_Repositories repositories; + const char *bootVersion; + + QString dalamudVersion; + int dalamudAssetVersion = -1; + QString runtimeVersion; + + QString expansionVersionText() const; + QString dalamudVersionText() const; + QString wineVersionText() const; + + [[nodiscard]] bool isGameInstalled() const + { + return repositories.repositories_count > 0; + } + + [[nodiscard]] bool isWineInstalled() const + { + return !m_wineVersion.isEmpty(); + } + +Q_SIGNALS: + void gameInstallChanged(); + void nameChanged(); + void languageChanged(); + void gamePathChanged(); + void winePathChanged(); + void winePrefixPathChanged(); + void enableWatchdogChanged(); + void wineTypeChanged(); + void useESyncChanged(); + void useGamescopeChanged(); + void useGamemodeChanged(); + void useDX9Changed(); + void gamescopeFullscreenChanged(); + void gamescopeBorderlessChanged(); + void gamescopeWidthChanged(); + void gamescopeHeightChanged(); + void gamescopeRefreshRateChanged(); + void dalamudEnabledChanged(); + void dalamudOptOutChanged(); + void dalamudChannelChanged(); + void encryptedArgumentsChanged(); + void accountChanged(); + void wineChanged(); + +private: + QString m_uuid; + QString m_wineVersion; + ProfileConfig m_config; + Account *m_account = nullptr; + LauncherCore &m_launcher; +}; \ No newline at end of file diff --git a/launcher/include/profilemanager.h b/launcher/include/profilemanager.h new file mode 100644 index 0000000..945e214 --- /dev/null +++ b/launcher/include/profilemanager.h @@ -0,0 +1,45 @@ +#pragma once + +#include + +#include "profile.h" + +class ProfileManager : public QAbstractListModel +{ + Q_OBJECT + +public: + explicit ProfileManager(LauncherCore &launcher, QObject *parent = nullptr); + + void load(); + + enum CustomRoles { + ProfileRole = Qt::UserRole, + }; + + int rowCount(const QModelIndex &index = QModelIndex()) const override; + + QVariant data(const QModelIndex &index, int role) const override; + + QHash roleNames() const override; + + Q_INVOKABLE Profile *getProfile(int index); + + int getProfileIndex(const QString &name); + Q_INVOKABLE Profile *addProfile(); + Q_INVOKABLE void deleteProfile(Profile *profile); + + QVector profiles() const; + + Q_INVOKABLE bool canDelete(Profile *account) const; + +private: + void insertProfile(Profile *profile); + + QString getDefaultGamePath(); + QString getDefaultWinePrefixPath(); + + QVector m_profiles; + + LauncherCore &m_launcher; +}; \ No newline at end of file diff --git a/launcher/include/sapphirelauncher.h b/launcher/include/sapphirelauncher.h new file mode 100644 index 0000000..d1dba6b --- /dev/null +++ b/launcher/include/sapphirelauncher.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +#include "launchercore.h" + +class SapphireLauncher : QObject +{ +public: + explicit SapphireLauncher(LauncherCore &window, QObject *parent = nullptr); + + void login(const QString &lobbyUrl, const LoginInformation &info); + void registerAccount(const QString &lobbyUrl, const LoginInformation &info); + +private: + LauncherCore &window; +}; \ No newline at end of file diff --git a/launcher/include/squareboot.h b/launcher/include/squareboot.h new file mode 100644 index 0000000..57d1824 --- /dev/null +++ b/launcher/include/squareboot.h @@ -0,0 +1,24 @@ +#pragma once + +#include "patcher.h" + +class SquareLauncher; +class LauncherCore; +class LoginInformation; + +class SquareBoot : public QObject +{ + Q_OBJECT +public: + SquareBoot(LauncherCore &window, SquareLauncher &launcher, QObject *parent = nullptr); + + Q_INVOKABLE void checkGateStatus(LoginInformation *info); + + void bootCheck(const LoginInformation &info); + +private: + Patcher *patcher = nullptr; + + LauncherCore &window; + SquareLauncher &launcher; +}; \ No newline at end of file diff --git a/launcher/include/squarelauncher.h b/launcher/include/squarelauncher.h new file mode 100644 index 0000000..b4cd631 --- /dev/null +++ b/launcher/include/squarelauncher.h @@ -0,0 +1,27 @@ +#pragma once + +#include "launchercore.h" +#include "patcher.h" + +class SquareLauncher : public QObject +{ + Q_OBJECT +public: + explicit SquareLauncher(LauncherCore &window, QObject *parent = nullptr); + + void getStored(const LoginInformation &info); + + void login(const LoginInformation &info, const QUrl &referer); + + void registerSession(const LoginInformation &info); + +private: + QString getBootHash(const LoginInformation &info); + + Patcher *patcher = nullptr; + + QString stored, SID, username; + LoginAuth auth; + + LauncherCore &window; +}; diff --git a/launcher/include/steamapi.h b/launcher/include/steamapi.h new file mode 100644 index 0000000..f3d5455 --- /dev/null +++ b/launcher/include/steamapi.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +class LauncherCore; + +class SteamAPI : public QObject +{ +public: + explicit SteamAPI(LauncherCore &core, QObject *parent = nullptr); + + void setLauncherMode(bool isLauncher); + + [[nodiscard]] bool isDeck() const; + +private: + LauncherCore &core; +}; \ No newline at end of file diff --git a/launcher/core/include/watchdog.h b/launcher/include/watchdog.h similarity index 50% rename from launcher/core/include/watchdog.h rename to launcher/include/watchdog.h index 1b3aacb..4b7f866 100644 --- a/launcher/core/include/watchdog.h +++ b/launcher/include/watchdog.h @@ -5,21 +5,26 @@ #include "launchercore.h" #if defined(Q_OS_LINUX) - #include "gameparser.h" +#include "gameparser.h" #endif #include -class Watchdog : public QObject { +class Watchdog : public QObject +{ Q_OBJECT public: - Watchdog(LauncherCore& core) : core(core), QObject(&core) {} + Watchdog(LauncherCore &core) + : core(core) + , QObject(&core) + { + } - void launchGame(const ProfileSettings& settings, const LoginAuth& auth); + void launchGame(const ProfileSettings &settings, const LoginAuth &auth); private: - LauncherCore& core; - QSystemTrayIcon* icon = nullptr; + LauncherCore &core; + QSystemTrayIcon *icon = nullptr; int processWindowId = -1; diff --git a/launcher/main.cpp b/launcher/main.cpp deleted file mode 100755 index 3ec4fba..0000000 --- a/launcher/main.cpp +++ /dev/null @@ -1,50 +0,0 @@ -#include "launchercore.h" - -#include -#include - -#include "config.h" -#include "desktopinterface.h" -#include "sapphirelauncher.h" -#include "squareboot.h" - -int main(int argc, char* argv[]) { - QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); - QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); - QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar); - - QApplication app(argc, argv); - - QCoreApplication::setApplicationName("astra"); - QCoreApplication::setApplicationVersion(version); - - QCommandLineParser parser; - parser.setApplicationDescription("Cross-platform FFXIV Launcher"); - - auto helpOption = parser.addHelpOption(); - auto versionOption = parser.addVersionOption(); - - QCommandLineOption steamOption("steam", "Simulate booting the launcher via Steam."); -#ifdef ENABLE_STEAM - parser.addOption(steamOption); -#endif - - parser.process(app); - - if (parser.isSet(versionOption)) { - parser.showVersion(); - } - - if (parser.isSet(helpOption)) { - parser.showHelp(); - } - -#ifdef ENABLE_STEAM - LauncherCore c(parser.isSet(steamOption)); -#else - LauncherCore c(false); -#endif - std::make_unique(c); - - return QApplication::exec(); -} diff --git a/launcher/profileconfig.kcfg b/launcher/profileconfig.kcfg new file mode 100644 index 0000000..877744d --- /dev/null +++ b/launcher/profileconfig.kcfg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + false + + + + + + + + + + + + + System + + + false + + + false + + + false + + + false + + + false + + + false + + + 1280 + + + 720 + + + 60 + + + false + + + false + + + + + + + + + + + Stable + + + true + + + diff --git a/launcher/profileconfig.kcfgc b/launcher/profileconfig.kcfgc new file mode 100644 index 0000000..4a905de --- /dev/null +++ b/launcher/profileconfig.kcfgc @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2023 Joshua Goins +# SPDX-License-Identifier: LGPL-2.1-or-later +File=profileconfig.kcfg +ClassName=ProfileConfig +Mutators=true +DefaultValueGetters=true +GenerateProperties=true +ParentInConstructor=true +Singleton=false diff --git a/launcher/resources.qrc b/launcher/resources.qrc new file mode 100644 index 0000000..0a0ee2b --- /dev/null +++ b/launcher/resources.qrc @@ -0,0 +1,21 @@ + + + ui/Components/FormFileDelegate.qml + ui/Components/FormFolderDelegate.qml + ui/Pages/LoginPage.qml + ui/Pages/NewsPage.qml + ui/Pages/StatusPage.qml + ui/Settings/AccountSettings.qml + ui/Settings/GeneralSettings.qml + ui/Settings/ProfileSettings.qml + ui/Settings/SettingsPage.qml + ui/Setup/AccountSetup.qml + ui/Setup/AddSapphire.qml + ui/Setup/AddSquareEnix.qml + ui/Setup/DownloadSetup.qml + ui/Setup/ExistingSetup.qml + ui/Setup/InstallProgress.qml + ui/Setup/SetupPage.qml + ui/main.qml + + \ No newline at end of file diff --git a/launcher/src/account.cpp b/launcher/src/account.cpp new file mode 100644 index 0000000..109c55b --- /dev/null +++ b/launcher/src/account.cpp @@ -0,0 +1,222 @@ +#include "account.h" + +#include +#include +#include +#include + +#include "cotp.h" +#include "launchercore.h" + +Account::Account(LauncherCore &launcher, const QString &key, QObject *parent) + : QObject(parent) + , m_config(key) + , m_key(key) + , m_launcher(launcher) +{ + fetchAvatar(); +} + +QString Account::uuid() const +{ + return m_key; +} + +QString Account::name() const +{ + return m_config.name(); +} + +void Account::setName(const QString &name) +{ + if (m_config.name() != name) { + m_config.setName(name); + m_config.save(); + Q_EMIT nameChanged(); + } +} + +QString Account::lodestoneId() const +{ + return m_config.lodestoneId(); +} + +void Account::setLodestoneId(const QString &id) +{ + if (m_config.lodestoneId() != id) { + m_config.setLodestoneId(id); + m_config.save(); + fetchAvatar(); + Q_EMIT lodestoneIdChanged(); + } +} + +QString Account::avatarUrl() const +{ + return m_url.toString(); +} + +bool Account::isSapphire() const +{ + return m_config.isSapphire(); +} + +void Account::setIsSapphire(bool value) +{ + if (m_config.isSapphire() != value) { + m_config.setIsSapphire(value); + m_config.save(); + Q_EMIT isSapphireChanged(); + } +} + +QString Account::lobbyUrl() const +{ + return m_config.lobbyUrl(); +} + +void Account::setLobbyUrl(const QString &value) +{ + if (m_config.lobbyUrl() != value) { + m_config.setLobbyUrl(value); + m_config.save(); + Q_EMIT lobbyUrlChanged(); + } +} + +bool Account::rememberPassword() const +{ + return m_config.rememberPassword(); +} + +void Account::setRememberPassword(const bool value) +{ + if (m_config.rememberPassword() != value) { + m_config.setRememberPassword(value); + m_config.save(); + Q_EMIT rememberPasswordChanged(); + } +} + +bool Account::rememberOTP() const +{ + return m_config.rememberOTP(); +} + +void Account::setRememberOTP(const bool value) +{ + if (m_config.rememberOTP() != value) { + m_config.setRememberOTP(value); + m_config.save(); + Q_EMIT rememberOTPChanged(); + } +} + +bool Account::useOTP() const +{ + return m_config.useOTP(); +} + +void Account::setUseOTP(const bool value) +{ + if (m_config.useOTP() != value) { + m_config.setUseOTP(value); + m_config.save(); + Q_EMIT useOTPChanged(); + } +} + +Account::GameLicense Account::license() const +{ + return static_cast(m_config.license()); +} + +void Account::setLicense(const GameLicense license) +{ + if (static_cast(m_config.license()) != license) { + m_config.setLicense(static_cast(license)); + m_config.save(); + Q_EMIT licenseChanged(); + } +} + +bool Account::isFreeTrial() const +{ + return m_config.isFreeTrial(); +} + +void Account::setIsFreeTrial(const bool value) +{ + if (m_config.isFreeTrial() != value) { + m_config.setIsFreeTrial(value); + m_config.save(); + Q_EMIT isFreeTrialChanged(); + } +} + +QString Account::getPassword() const +{ + return getKeychainValue("password"); +} + +void Account::setPassword(const QString &password) +{ + setKeychainValue("password", password); +} + +QString Account::getOTP() const +{ + auto otpSecret = getKeychainValue("otp-secret"); + + char *totp = get_totp(otpSecret.toStdString().c_str(), 6, 30, SHA1, nullptr); + QString totpStr(totp); + free(totp); + + return totpStr; +} + +void Account::fetchAvatar() +{ + if (lodestoneId().isEmpty()) { + return; + } + + QNetworkRequest request(QStringLiteral("https://xivapi.com/character/%1").arg(lodestoneId())); + auto reply = m_launcher.mgr->get(request); + connect(reply, &QNetworkReply::finished, [this, reply] { + auto document = QJsonDocument::fromJson(reply->readAll()); + if (document.isObject()) { + m_url = document.object()["Character"].toObject()["Avatar"].toString(); + Q_EMIT avatarUrlChanged(); + } + }); +} + +void Account::setKeychainValue(const QString &key, const QString &value) const +{ + auto job = new QKeychain::WritePasswordJob("Astra"); + job->setTextData(value); + job->setKey(m_key + "-" + key); + job->start(); +} + +QString Account::getKeychainValue(const QString &key) const +{ + auto loop = new QEventLoop(); + + auto job = new QKeychain::ReadPasswordJob("Astra"); + job->setKey(m_key + "-" + key); + job->start(); + + QString value; + + QObject::connect(job, &QKeychain::ReadPasswordJob::finished, [loop, job, &value](QKeychain::Job *j) { + Q_UNUSED(j) + value = job->textData(); + loop->quit(); + }); + + loop->exec(); + + return value; +} diff --git a/launcher/src/accountmanager.cpp b/launcher/src/accountmanager.cpp new file mode 100644 index 0000000..f16f8aa --- /dev/null +++ b/launcher/src/accountmanager.cpp @@ -0,0 +1,106 @@ +#include "accountmanager.h" + +#include + +AccountManager::AccountManager(LauncherCore &launcher, QObject *parent) + : QAbstractListModel(parent) + , m_launcher(launcher) +{ +} + +void AccountManager::load() +{ + auto config = KSharedConfig::openStateConfig(); + for (const auto &id : config->groupList()) { + if (id.contains("account-")) { + auto profile = new Account(m_launcher, QString(id).remove("account-"), this); + m_accounts.append(profile); + } + } +} + +int AccountManager::rowCount(const QModelIndex &index) const +{ + Q_UNUSED(index); + return m_accounts.size(); +} + +QVariant AccountManager::data(const QModelIndex &index, int role) const +{ + if (!checkIndex(index)) { + return {}; + } + + const int row = index.row(); + if (role == AccountRole) { + return QVariant::fromValue(m_accounts[row]); + } + + return {}; +} + +QHash AccountManager::roleNames() const +{ + return {{AccountRole, QByteArrayLiteral("account")}}; +} + +Account *AccountManager::createSquareEnixAccount(const QString &username, int licenseType, bool isFreeTrial) +{ + auto account = new Account(m_launcher, QUuid::createUuid().toString(), this); + account->setIsSapphire(false); + account->setLicense(static_cast(licenseType)); + account->setIsFreeTrial(isFreeTrial); + account->setName(username); + + insertAccount(account); + + return account; +} + +Account *AccountManager::createSapphireAccount(const QString &lobbyUrl, const QString &username) +{ + auto account = new Account(m_launcher, QUuid::createUuid().toString(), this); + account->setIsSapphire(true); + account->setName(username); + account->setLobbyUrl(lobbyUrl); + + insertAccount(account); + + return account; +} + +Account *AccountManager::getByUuid(const QString &uuid) const +{ + for (auto &account : m_accounts) { + if (account->uuid() == uuid) { + return account; + } + } + + return nullptr; +} + +bool AccountManager::canDelete(Account *account) const +{ + Q_UNUSED(account) + return m_accounts.size() != 1; +} + +void AccountManager::deleteAccount(Account *account) +{ + auto config = KSharedConfig::openStateConfig(); + config->deleteGroup(QString("account-%1").arg(account->uuid())); + config->sync(); + + const int row = m_accounts.indexOf(account); + beginRemoveRows(QModelIndex(), row, row); + m_accounts.removeAll(account); + endRemoveRows(); +} + +void AccountManager::insertAccount(Account *account) +{ + beginInsertRows(QModelIndex(), m_accounts.size(), m_accounts.size()); + m_accounts.append(account); + endInsertRows(); +} diff --git a/launcher/core/src/assetupdater.cpp b/launcher/src/assetupdater.cpp similarity index 76% rename from launcher/core/src/assetupdater.cpp rename to launcher/src/assetupdater.cpp index 8521d36..bd6776a 100644 --- a/launcher/core/src/assetupdater.cpp +++ b/launcher/src/assetupdater.cpp @@ -3,14 +3,11 @@ #include #include #include -#include #include #include #include -#include "launchercore.h" - const QString baseGoatDomain = "https://goatcorp.github.io"; const QString baseDalamudDistribution = baseGoatDomain + "/dalamud-distrib/"; @@ -20,17 +17,18 @@ const QString dalamudVersionManifestURL = baseDalamudDistribution + "%1version"; const QString baseDalamudAssetDistribution = baseGoatDomain + "/DalamudAssets"; const QString dalamudAssetManifestURL = baseDalamudAssetDistribution + "/asset.json"; -const QString dotnetRuntimePackageURL = - "https://dotnetcli.azureedge.net/dotnet/Runtime/%1/dotnet-runtime-%1-win-x64.zip"; -const QString dotnetDesktopPackageURL = - "https://dotnetcli.azureedge.net/dotnet/WindowsDesktop/%1/windowsdesktop-runtime-%1-win-x64.zip"; +const QString dotnetRuntimePackageURL = "https://dotnetcli.azureedge.net/dotnet/Runtime/%1/dotnet-runtime-%1-win-x64.zip"; +const QString dotnetDesktopPackageURL = "https://dotnetcli.azureedge.net/dotnet/WindowsDesktop/%1/windowsdesktop-runtime-%1-win-x64.zip"; -QMap channelToDistribPrefix = { - {DalamudChannel::Stable, "/"}, - {DalamudChannel::Staging, "stg/"}, - {DalamudChannel::Net5, "net5/"}}; +QMap channelToDistribPrefix = {{Profile::DalamudChannel::Stable, "/"}, + {Profile::DalamudChannel::Staging, "stg/"}, + {Profile::DalamudChannel::Net5, "net5/"}}; -AssetUpdater::AssetUpdater(LauncherCore& launcher) : launcher(launcher), QObject(&launcher) { +AssetUpdater::AssetUpdater(Profile &profile, LauncherCore &launcher, QObject *parent) + : QObject(parent) + , launcher(launcher) + , m_profile(profile) +{ launcher.mgr->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy); dataDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); @@ -39,19 +37,20 @@ AssetUpdater::AssetUpdater(LauncherCore& launcher) : launcher(launcher), QObject QDir().mkdir(dataDir); } -void AssetUpdater::update(const ProfileSettings& profile) { +void AssetUpdater::update() +{ // non-dalamud users can bypass this process since it's not needed - if (!profile.dalamud.enabled) { + if (!m_profile.dalamudEnabled()) { finishedUpdating(); return; } - dialog = new QProgressDialog("Updating assets...", "Cancel", 0, 0); + // dialog = new QProgressDialog("Updating assets...", "Cancel", 0, 0); // first, we want to collect all of the remote versions qInfo() << "Starting update sequence..."; - dialog->setLabelText("Checking for updates..."); + // dialog->setLabelText("Checking for updates..."); // dalamud assets { @@ -65,14 +64,14 @@ void AssetUpdater::update(const ProfileSettings& profile) { QNetworkRequest request(dalamudAssetManifestURL); auto reply = launcher.mgr->get(request); - connect(reply, &QNetworkReply::finished, [reply, this, &profile] { - dialog->setLabelText("Checking for Dalamud asset updates..."); + connect(reply, &QNetworkReply::finished, [reply, this] { + // dialog->setLabelText("Checking for Dalamud asset updates..."); // TODO: handle asset failure QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); qInfo() << "Dalamud asset remote version" << doc.object()["Version"].toInt(); - qInfo() << "Dalamud asset local version" << launcher.dalamudAssetVersion; + qInfo() << "Dalamud asset local version" << m_profile.dalamudAssetVersion; remoteDalamudAssetVersion = doc.object()["Version"].toInt(); @@ -85,16 +84,16 @@ void AssetUpdater::update(const ProfileSettings& profile) { // dalamud injector / net runtime // they're all updated in unison, so there's no reason to have multiple checks { - QNetworkRequest request(dalamudVersionManifestURL.arg(channelToDistribPrefix[profile.dalamud.channel])); + QNetworkRequest request(dalamudVersionManifestURL.arg(channelToDistribPrefix[m_profile.dalamudChannel()])); - chosenChannel = profile.dalamud.channel; + chosenChannel = m_profile.dalamudChannel(); remoteDalamudVersion.clear(); remoteRuntimeVersion.clear(); auto reply = launcher.mgr->get(request); - connect(reply, &QNetworkReply::finished, [this, &profile, reply] { - dialog->setLabelText("Checking for Dalamud updates..."); + connect(reply, &QNetworkReply::finished, [this, reply] { + // dialog->setLabelText("Checking for Dalamud updates..."); QByteArray str = reply->readAll(); // for some god forsaken reason, the version string comes back as raw @@ -119,7 +118,8 @@ void AssetUpdater::update(const ProfileSettings& profile) { } } -void AssetUpdater::beginInstall() { +void AssetUpdater::beginInstall() +{ if (needsDalamudInstall) { bool success = !JlCompress::extractDir(tempDir.path() + "/latest.zip", dataDir + "/Dalamud").empty(); @@ -132,8 +132,7 @@ 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", dataDir + "/DalamudRuntime").empty(); success |= !JlCompress::extractDir(tempDir.path() + "/dotnet-desktop.zip", dataDir + "/DalamudRuntime").empty(); @@ -152,14 +151,15 @@ void AssetUpdater::beginInstall() { checkIfFinished(); } -void AssetUpdater::checkIfDalamudAssetsDone() { - if (dialog->wasCanceled()) - return; +void AssetUpdater::checkIfDalamudAssetsDone() +{ + // if (dialog->wasCanceled()) + // return; if (dalamudAssetNeededFilenames.empty()) { qInfo() << "Finished downloading Dalamud assets."; - launcher.dalamudAssetVersion = remoteDalamudAssetVersion; + m_profile.dalamudAssetVersion = remoteDalamudAssetVersion; QFile file(dataDir + "/DalamudAssets/" + "asset.ver"); file.open(QIODevice::WriteOnly | QIODevice::Text); @@ -170,26 +170,27 @@ void AssetUpdater::checkIfDalamudAssetsDone() { } } -void AssetUpdater::checkIfFinished() { - if (dialog->wasCanceled()) - return; +void AssetUpdater::checkIfFinished() +{ + // if (dialog->wasCanceled()) + // return; - if (doneDownloadingDalamud && doneDownloadingRuntimeCore && - doneDownloadingRuntimeDesktop && dalamudAssetNeededFilenames.empty()) { + if (doneDownloadingDalamud && doneDownloadingRuntimeCore && doneDownloadingRuntimeDesktop && dalamudAssetNeededFilenames.empty()) { if (needsRuntimeInstall || needsDalamudInstall) { beginInstall(); } else { - dialog->setLabelText("Finished!"); - dialog->close(); + // dialog->setLabelText("Finished!"); + // dialog->close(); finishedUpdating(); } } } -void AssetUpdater::checkIfCheckingIsDone() { - if (dialog->wasCanceled()) - return; +void AssetUpdater::checkIfCheckingIsDone() +{ + // if (dialog->wasCanceled()) + // return; if (remoteDalamudVersion.isEmpty() || remoteRuntimeVersion.isEmpty() || remoteDalamudAssetVersion == -1) { return; @@ -198,10 +199,10 @@ void AssetUpdater::checkIfCheckingIsDone() { // now that we got all the information we need, let's check if anything is // updateable - dialog->setLabelText("Starting update..."); + // dialog->setLabelText("Starting update..."); // dalamud injector / net runtime - if (launcher.runtimeVersion != remoteRuntimeVersion) { + if (m_profile.runtimeVersion != remoteRuntimeVersion) { needsRuntimeInstall = true; // core @@ -212,7 +213,7 @@ void AssetUpdater::checkIfCheckingIsDone() { connect(reply, &QNetworkReply::finished, [this, reply] { qInfo() << "Dotnet-core finished downloading!"; - dialog->setLabelText("Updating Dotnet-core..."); + // dialog->setLabelText("Updating Dotnet-core..."); QFile file(tempDir.path() + "/dotnet-core.zip"); file.open(QIODevice::WriteOnly); @@ -233,7 +234,7 @@ void AssetUpdater::checkIfCheckingIsDone() { connect(reply, &QNetworkReply::finished, [this, reply] { qInfo() << "Dotnet-desktop finished downloading!"; - dialog->setLabelText("Updating Dotnet-desktop..."); + // dialog->setLabelText("Updating Dotnet-desktop..."); QFile file(tempDir.path() + "/dotnet-desktop.zip"); file.open(QIODevice::WriteOnly); @@ -253,7 +254,7 @@ void AssetUpdater::checkIfCheckingIsDone() { checkIfFinished(); } - if (remoteDalamudVersion != launcher.dalamudVersion) { + if (remoteDalamudVersion != m_profile.dalamudVersion) { qInfo() << "Downloading Dalamud..."; needsDalamudInstall = true; @@ -264,7 +265,7 @@ void AssetUpdater::checkIfCheckingIsDone() { connect(reply, &QNetworkReply::finished, [this, reply] { qInfo() << "Dalamud finished downloading!"; - dialog->setLabelText("Updating Dalamud..."); + // dialog->setLabelText("Updating Dalamud..."); QFile file(tempDir.path() + "/latest.zip"); file.open(QIODevice::WriteOnly); @@ -273,7 +274,7 @@ void AssetUpdater::checkIfCheckingIsDone() { doneDownloadingDalamud = true; - launcher.dalamudVersion = remoteDalamudVersion; + m_profile.dalamudVersion = remoteDalamudVersion; checkIfFinished(); }); @@ -287,10 +288,10 @@ void AssetUpdater::checkIfCheckingIsDone() { } // dalamud assets - if (remoteDalamudAssetVersion != launcher.dalamudAssetVersion) { + if (remoteDalamudAssetVersion != m_profile.dalamudAssetVersion) { qInfo() << "Dalamud assets out of date."; - dialog->setLabelText("Updating Dalamud assets..."); + // dialog->setLabelText("Updating Dalamud assets..."); dalamudAssetNeededFilenames.clear(); @@ -309,7 +310,7 @@ void AssetUpdater::checkIfCheckingIsDone() { const QList dirPath = fileName.left(fileName.lastIndexOf("/")).split('/'); QString build = dataDir + "/DalamudAssets/"; - for (const auto& dir : dirPath) { + for (const auto &dir : dirPath) { if (!QDir().exists(build + dir)) QDir().mkdir(build + dir); diff --git a/launcher/src/encryptedarg.cpp b/launcher/src/encryptedarg.cpp new file mode 100644 index 0000000..0004ea9 --- /dev/null +++ b/launcher/src/encryptedarg.cpp @@ -0,0 +1,81 @@ +#include "encryptedarg.h" + +#include + +#if defined(Q_OS_MAC) +#include +#include +#endif + +#if defined(Q_OS_WIN) +#include +#endif + +#if defined(Q_OS_MAC) +// taken from XIV-on-Mac, apparently Wine changed this? +uint32_t TickCount() +{ + struct mach_timebase_info timebase; + mach_timebase_info(&timebase); + + auto machtime = mach_continuous_time(); + auto numer = uint64_t(timebase.numer); + auto denom = uint64_t(timebase.denom); + auto monotonic_time = machtime * numer / denom / 100; + return monotonic_time / 10000; +} +#endif + +#if defined(Q_OS_LINUX) +uint32_t TickCount() +{ + struct timespec ts { + }; + + clock_gettime(CLOCK_MONOTONIC, &ts); + + return (ts.tv_sec * 1000 + ts.tv_nsec / 1000000); +} +#endif + +#if defined(Q_OS_WIN) +uint32_t TickCount() +{ + return GetTickCount(); +} +#endif + +// from xivdev +static char ChecksumTable[] = {'f', 'X', '1', 'p', 'G', 't', 'd', 'S', '5', 'C', 'A', 'P', '4', '_', 'V', 'L'}; + +inline char GetChecksum(const unsigned int key) +{ + auto value = key & 0x000F0000; + return ChecksumTable[value >> 16]; +} + +QString encryptGameArg(const QString &arg) +{ + const uint32_t rawTicks = TickCount(); + const uint32_t ticks = rawTicks & 0xFFFFFFFFu; + const uint32_t key = ticks & 0xFFFF0000u; + + char buffer[9]{}; + sprintf(buffer, "%08x", key); + + Blowfish const *blowfish = physis_blowfish_initialize(reinterpret_cast(buffer), 9); + + uint8_t *out_data = nullptr; + uint32_t out_size = 0; + + QByteArray toEncrypt = (QString(" /T =%1").arg(ticks) + arg).toUtf8(); + + physis_blowfish_encrypt(blowfish, reinterpret_cast(toEncrypt.data()), toEncrypt.size(), &out_data, &out_size); + + const QByteArray encryptedArg = QByteArray::fromRawData(reinterpret_cast(out_data), static_cast(out_size)); + + const QString base64 = encryptedArg.toBase64(QByteArray::Base64Option::Base64UrlEncoding | QByteArray::Base64Option::KeepTrailingEquals); + const char checksum = GetChecksum(key); + + return QString("//**sqex0003%1%2**//").arg(base64, QString(checksum)); +} \ No newline at end of file diff --git a/launcher/core/src/gameinstaller.cpp b/launcher/src/gameinstaller.cpp similarity index 66% rename from launcher/core/src/gameinstaller.cpp rename to launcher/src/gameinstaller.cpp index 8ec1df9..a9ee79e 100644 --- a/launcher/core/src/gameinstaller.cpp +++ b/launcher/src/gameinstaller.cpp @@ -6,17 +6,25 @@ #include #include "launchercore.h" +#include "profile.h" -void installGame(LauncherCore& launcher, ProfileSettings& profile, const std::function& returnFunc) { - QString installDirectory = profile.gamePath; +GameInstaller::GameInstaller(LauncherCore &launcher, Profile &profile, QObject *parent) + : QObject(parent) + , m_launcher(launcher) + , m_profile(profile) +{ +} + +void GameInstaller::installGame() +{ + const QString installDirectory = m_profile.gamePath(); qDebug() << "Installing game to " << installDirectory << "!"; - qDebug() << "Now downloading installer file..."; QNetworkRequest request(QUrl("https://gdl.square-enix.com/ffxiv/inst/ffxivsetup.exe")); - auto reply = launcher.mgr->get(request); - QObject::connect(reply, &QNetworkReply::finished, [reply, installDirectory, returnFunc] { + auto reply = m_launcher.mgr->get(request); + QObject::connect(reply, &QNetworkReply::finished, [this, reply, installDirectory] { QString dataDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); QFile file(dataDir + "/ffxivsetup.exe"); @@ -31,6 +39,6 @@ void installGame(LauncherCore& launcher, ProfileSettings& profile, const std::fu qDebug() << "Done installing to " << installDirectory << "!"; - returnFunc(); + Q_EMIT installFinished(); }); -} \ No newline at end of file +} diff --git a/launcher/core/src/gameparser.cpp b/launcher/src/gameparser.cpp similarity index 93% rename from launcher/core/src/gameparser.cpp rename to launcher/src/gameparser.cpp index eb38cc8..92fdad5 100644 --- a/launcher/core/src/gameparser.cpp +++ b/launcher/src/gameparser.cpp @@ -4,7 +4,8 @@ #include #include -GameParser::GameParser() { +GameParser::GameParser() +{ api = new tesseract::TessBaseAPI(); if (api->Init(nullptr, "eng")) { @@ -15,17 +16,19 @@ GameParser::GameParser() { api->SetPageSegMode(tesseract::PageSegMode::PSM_SINGLE_BLOCK); } -GameParser::~GameParser() { +GameParser::~GameParser() +{ api->End(); delete api; } -GameParseResult GameParser::parseImage(QImage img) { +GameParseResult GameParser::parseImage(QImage img) +{ QBuffer buf; img = img.convertToFormat(QImage::Format_Grayscale8); img.save(&buf, "PNG", 100); - Pix* image = pixReadMem((const l_uint8*)buf.data().data(), buf.size()); + Pix *image = pixReadMem((const l_uint8 *)buf.data().data(), buf.size()); api->SetImage(image); api->SetSourceResolution(300); diff --git a/launcher/src/launchercore.cpp b/launcher/src/launchercore.cpp new file mode 100755 index 0000000..bf0e192 --- /dev/null +++ b/launcher/src/launchercore.cpp @@ -0,0 +1,580 @@ +#include "gameinstaller.h" + +#include +#include +#include +#include +#include +#include + +#ifdef ENABLE_GAMEMODE +#include +#endif + +#include "account.h" +#include "assetupdater.h" +#include "config.h" +#include "encryptedarg.h" +#include "launchercore.h" +#include "sapphirelauncher.h" +#include "squarelauncher.h" + +#ifdef ENABLE_WATCHDOG +#include "watchdog.h" +#endif + +void LauncherCore::setSSL(QNetworkRequest &request) +{ + QSslConfiguration config; + config.setProtocol(QSsl::AnyProtocol); + config.setPeerVerifyMode(QSslSocket::VerifyNone); + + request.setSslConfiguration(config); +} + +void LauncherCore::buildRequest(const Profile &settings, QNetworkRequest &request) +{ + setSSL(request); + + if (settings.account()->license() == Account::GameLicense::macOS) { + request.setHeader(QNetworkRequest::UserAgentHeader, "macSQEXAuthor/2.0.0(MacOSX; ja-jp)"); + } else { + request.setHeader(QNetworkRequest::UserAgentHeader, QString("SQEXAuthor/2.0.0(Windows 6.2; ja-jp; %1)").arg(QString(QSysInfo::bootUniqueId()))); + } + + request.setRawHeader("Accept", + "image/gif, image/jpeg, image/pjpeg, application/x-ms-application, application/xaml+xml, " + "application/x-ms-xbap, */*"); + request.setRawHeader("Accept-Encoding", "gzip, deflate"); + request.setRawHeader("Accept-Language", "en-us"); +} + +void LauncherCore::launchGame(const Profile &profile, const LoginAuth &auth) +{ + steamApi->setLauncherMode(false); + +#ifdef ENABLE_WATCHDOG + if (profile.enableWatchdog) { + watchdog->launchGame(profile, auth); + } else { + beginGameExecutable(profile, auth); + } +#else + beginGameExecutable(profile, auth); +#endif +} + +void LauncherCore::beginGameExecutable(const Profile &profile, const LoginAuth &auth) +{ + QString gameExectuable; + if (profile.directx9Enabled()) { + gameExectuable = profile.gamePath() + "/game/ffxiv.exe"; + } else { + gameExectuable = profile.gamePath() + "/game/ffxiv_dx11.exe"; + } + + if (profile.dalamudEnabled()) { + beginDalamudGame(gameExectuable, profile, auth); + } else { + beginVanillaGame(gameExectuable, profile, auth); + } + + successfulLaunch(); +} + +void LauncherCore::beginVanillaGame(const QString &gameExecutablePath, const Profile &profile, const LoginAuth &auth) +{ + auto gameProcess = new QProcess(); + gameProcess->setProcessEnvironment(QProcessEnvironment::systemEnvironment()); + + auto args = getGameArgs(profile, auth); + + launchExecutable(profile, gameProcess, {gameExecutablePath, args}, true, true); +} + +void LauncherCore::beginDalamudGame(const QString &gameExecutablePath, const Profile &profile, const LoginAuth &auth) +{ + QString gamePath = gameExecutablePath; + gamePath = "Z:" + gamePath.replace('/', '\\'); + + QString dataDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + dataDir = "Z:" + dataDir.replace('/', '\\'); + + auto dalamudProcess = new QProcess(); + + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert("DALAMUD_RUNTIME", dataDir + "\\DalamudRuntime"); + +#if defined(Q_OS_LINUX) || defined(Q_OS_MAC) + env.insert("XL_WINEONLINUX", "true"); +#endif + dalamudProcess->setProcessEnvironment(env); + + auto args = getGameArgs(profile, auth); + + launchExecutable(profile, + dalamudProcess, + {dataDir + "/Dalamud/" + "Dalamud.Injector.exe", + "launch", + "-m", + "inject", + "--game=" + gamePath, + "--dalamud-configuration-path=" + dataDir + "\\dalamudConfig.json", + "--dalamud-plugin-directory=" + dataDir + "\\installedPlugins", + "--dalamud-asset-directory=" + dataDir + "\\DalamudAssets", + "--dalamud-client-language=" + QString::number(profile.language()), + "--", + args}, + true, + true); +} + +QString LauncherCore::getGameArgs(const Profile &profile, const LoginAuth &auth) +{ + struct Argument { + QString key, value; + }; + + QList gameArgs; + gameArgs.push_back({"DEV.DataPathType", QString::number(1)}); + gameArgs.push_back({"DEV.UseSqPack", QString::number(1)}); + + gameArgs.push_back({"DEV.MaxEntitledExpansionID", QString::number(auth.maxExpansion)}); + gameArgs.push_back({"DEV.TestSID", auth.SID}); + 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}); + + if (!auth.lobbyhost.isEmpty()) { + gameArgs.push_back({"DEV.GMServerHost", auth.frontierHost}); + for (int i = 1; i < 9; i++) { + gameArgs.push_back({QString("DEV.LobbyHost0%1").arg(QString::number(i)), auth.lobbyhost}); + gameArgs.push_back({QString("DEV.LobbyPort0%1").arg(QString::number(i)), QString::number(54994)}); + } + } + + if (profile.account()->license() == Account::GameLicense::WindowsSteam) { + gameArgs.push_back({"IsSteam", "1"}); + } + + const QString argFormat = profile.argumentsEncrypted() ? " /%1 =%2" : " %1=%2"; + + QString argJoined; + for (const auto &arg : gameArgs) { + argJoined += argFormat.arg(arg.key, arg.value); + } + + return profile.argumentsEncrypted() ? encryptGameArg(argJoined) : argJoined; +} + +void LauncherCore::launchExecutable(const Profile &profile, QProcess *process, const QStringList &args, bool isGame, bool needsRegistrySetup) +{ + QList arguments; + auto env = QProcessEnvironment::systemEnvironment(); + + if (needsRegistrySetup) { +#if defined(Q_OS_LINUX) || defined(Q_OS_MAC) + if (profile.account()->license() == Account::GameLicense::macOS) { + addRegistryKey(profile, "HKEY_CURRENT_USER\\Software\\Wine", "HideWineExports", "0"); + } else { + addRegistryKey(profile, "HKEY_CURRENT_USER\\Software\\Wine", "HideWineExports", "1"); + } +#endif + } + +#if defined(Q_OS_LINUX) + if (isGame) { + if (profile.gamescopeEnabled()) { + arguments.push_back("gamescope"); + + if (profile.gamescopeFullscreen()) + arguments.push_back("-f"); + + if (profile.gamescopeBorderless()) + arguments.push_back("-b"); + + if (profile.gamescopeWidth() > 0) + arguments.push_back("-w " + QString::number(profile.gamescopeWidth())); + + if (profile.gamescopeHeight() > 0) + arguments.push_back("-h " + QString::number(profile.gamescopeHeight())); + + if (profile.gamescopeRefreshRate() > 0) + arguments.push_back("-r " + QString::number(profile.gamescopeRefreshRate())); + } + } +#endif + +#ifdef ENABLE_GAMEMODE + if (isGame && profile.useGamemode) { + gamemode_request_start(); + } +#endif + +#if defined(Q_OS_LINUX) + if (profile.esyncEnabled()) { + env.insert("WINEESYNC", QString::number(1)); + env.insert("WINEFSYNC", QString::number(1)); + env.insert("WINEFSYNC_FUTEX2", QString::number(1)); + } +#endif + +#if defined(Q_OS_MAC) || defined(Q_OS_LINUX) + if (m_isSteam) { + const QString steamDirectory = QProcessEnvironment::systemEnvironment().value("STEAM_COMPAT_CLIENT_INSTALL_PATH"); + const QString compatData = + QProcessEnvironment::systemEnvironment().value("STEAM_COMPAT_DATA_PATH"); // TODO: do these have to exist on the root steam folder? + const QString protonPath = steamDirectory + "/steamapps/common/Proton 7.0"; + + // env.insert("PATH", protonPath + "/dist/bin:" + QProcessEnvironment::systemEnvironment().value("PATH")); + // env.insert("WINEDLLPATH", protonPath + "/dist/lib64/wine:" + protonPath + "/dist/lib/wine"); + // env.insert("LD_LIBRARY_PATH", protonPath + "/dist/lib64:" + protonPath + "/dist/lib"); + // env.insert("WINEPREFIX", compatData + "/pfx"); + env.insert("STEAM_COMPAT_CLIENT_INSTALL_PATH", steamDirectory); + env.insert("STEAM_COMPAT_DATA_PATH", compatData); + + qInfo() << env.toStringList(); + + arguments.push_back(protonPath + "/proton"); + arguments.push_back("run"); + } else { + env.insert("WINEPREFIX", profile.winePrefixPath()); + + // XIV on Mac bundle their own Wine install directory, complete with libs etc + if (profile.wineType() == Profile::WineType::XIVOnMac) { + // TODO: don't hardcode this + QString xivLibPath = + "/Applications/XIV on Mac.app/Contents/Resources/wine/lib:/Applications/XIV on " + "Mac.app/Contents/Resources/MoltenVK/modern"; + + env.insert("DYLD_FALLBACK_LIBRARY_PATH", xivLibPath); + env.insert("DYLD_VERSIONED_LIBRARY_PATH", xivLibPath); + env.insert("MVK_CONFIG_FULL_IMAGE_VIEW_SWIZZLE", "1"); + env.insert("MVK_CONFIG_RESUME_LOST_DEVICE", "1"); + env.insert("MVK_ALLOW_METAL_FENCES", "1"); + env.insert("MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS", "1"); + } + +#if defined(FLATPAK) + arguments.push_back("flatpak-spawn"); + arguments.push_back("--host"); +#endif + arguments.push_back(profile.winePath()); + } +#endif + + arguments.append(args); + + qInfo() << arguments; + + auto executable = arguments[0]; + arguments.removeFirst(); + + if (isGame) + process->setWorkingDirectory(profile.gamePath() + "/game/"); + + process->setProcessEnvironment(env); + + process->start(executable, arguments); +} + +void LauncherCore::readInitialInformation() +{ + gamescopeAvailable = checkIfInPath("gamescope"); + gamemodeAvailable = checkIfInPath("gamemoderun"); + + m_profileManager->load(); + m_accountManager->load(); + + // restore profile -> account connections + for (auto profile : m_profileManager->profiles()) { + if (auto account = m_accountManager->getByUuid(profile->accountUuid())) { + profile->setAccount(account); + } + } + + m_loadingFinished = true; + Q_EMIT loadingFinished(); +} + +LauncherCore::LauncherCore(bool isSteam) + : m_isSteam(isSteam) +{ + mgr = new QNetworkAccessManager(); + sapphireLauncher = new SapphireLauncher(*this); + squareLauncher = new SquareLauncher(*this); + squareBoot = new SquareBoot(*this, *squareLauncher); + // assetUpdater = new AssetUpdater(*this); + steamApi = new SteamAPI(*this); + m_profileManager = new ProfileManager(*this); + m_accountManager = new AccountManager(*this); + +#ifdef ENABLE_WATCHDOG + watchdog = new Watchdog(*this); +#endif + + readInitialInformation(); + + steamApi->setLauncherMode(true); +} + +bool LauncherCore::checkIfInPath(const QString &program) +{ + // TODO: also check /usr/local/bin, /bin32 etc (basically read $PATH) + const QString directory = "/usr/bin"; + + QFileInfo fileInfo(directory + "/" + program); + + return fileInfo.exists() && fileInfo.isFile(); +} + +void LauncherCore::addRegistryKey(const Profile &settings, QString key, QString value, QString data) +{ + auto process = new QProcess(this); + process->setProcessEnvironment(QProcessEnvironment::systemEnvironment()); + launchExecutable(settings, process, {"reg", "add", std::move(key), "/v", std::move(value), "/d", std::move(data), "/f"}, false, false); +} + +void LauncherCore::login(Profile *profile, const QString &username, const QString &password, const QString &oneTimePassword) +{ + auto loginInformation = new LoginInformation(); + loginInformation->profile = profile; + loginInformation->username = username; + loginInformation->password = password; + loginInformation->oneTimePassword = oneTimePassword; + + if (profile->account()->rememberPassword()) { + profile->account()->setPassword(password); + } + + if (loginInformation->profile->account()->isSapphire()) { + sapphireLauncher->login(loginInformation->profile->account()->lobbyUrl(), *loginInformation); + } else { + squareBoot->checkGateStatus(loginInformation); + } +} + +bool LauncherCore::autoLogin(Profile &profile) +{ + // TODO: when login fails, we need some way to propagate this back? or not? + login(&profile, profile.account()->name(), profile.account()->getPassword(), profile.account()->getOTP()); + + return true; +} + +GameInstaller *LauncherCore::createInstaller(Profile &profile) +{ + return new GameInstaller(*this, profile, this); +} + +bool LauncherCore::isLoadingFinished() const +{ + return m_loadingFinished; +} + +bool LauncherCore::hasAccount() const +{ + return false; +} + +ProfileManager *LauncherCore::profileManager() +{ + return m_profileManager; +} + +AccountManager *LauncherCore::accountManager() +{ + return m_accountManager; +} + +bool LauncherCore::closeWhenLaunched() const +{ + return Config::closeWhenLaunched(); +} + +void LauncherCore::setCloseWhenLaunched(const bool value) +{ + if (value != Config::closeWhenLaunched()) { + Config::setCloseWhenLaunched(value); + Config::self()->save(); + Q_EMIT closeWhenLaunchedChanged(); + } +} + +bool LauncherCore::showNewsBanners() const +{ + return Config::showNewsBanners(); +} + +void LauncherCore::setShowNewsBanners(const bool value) +{ + if (value != Config::showNewsBanners()) { + Config::setShowNewsBanners(value); + Config::self()->save(); + Q_EMIT showNewsBannersChanged(); + } +} + +bool LauncherCore::showNewsList() const +{ + return Config::showNewsList(); +} + +void LauncherCore::setShowNewsList(const bool value) +{ + if (value != Config::showNewsList()) { + Config::setShowNewsList(value); + Config::self()->save(); + Q_EMIT showNewsListChanged(); + } +} + +void LauncherCore::refreshNews() +{ + QUrlQuery query; + query.addQueryItem("lang", "en-us"); + query.addQueryItem("media", "pcapp"); + + QUrl url; + url.setScheme("https"); + url.setHost("frontier.ffxiv.com"); + url.setPath("/news/headline.json"); + url.setQuery(query); + + auto request = QNetworkRequest(QString("%1&%2").arg(url.toString(), QString::number(QDateTime::currentMSecsSinceEpoch()))); + + // TODO: really? + buildRequest(*profileManager()->getProfile(0), request); + + request.setRawHeader("Accept", "application/json, text/plain, */*"); + request.setRawHeader("Origin", "https://launcher.finalfantasyxiv.com"); + request.setRawHeader("Referer", + QString("https://launcher.finalfantasyxiv.com/v600/index.html?rc_lang=%1&time=%2") + .arg("en-us", QDateTime::currentDateTimeUtc().toString("yyyy-MM-dd-HH")) + .toUtf8()); + + auto reply = mgr->get(request); + QObject::connect(reply, &QNetworkReply::finished, [this, reply] { + auto document = QJsonDocument::fromJson(reply->readAll()); + + auto headline = new Headline(); + + const auto parseNews = [](QJsonObject object) -> News { + News news; + news.date = QDateTime::fromString(object["date"].toString(), Qt::DateFormat::ISODate); + news.id = object["id"].toString(); + news.tag = object["tag"].toString(); + news.title = object["title"].toString(); + + if (object["url"].toString().isEmpty()) { + news.url = QUrl(QString("https://na.finalfantasyxiv.com/lodestone/news/detail/%1").arg(news.id)); + } else { + news.url = QUrl(object["url"].toString()); + } + + return news; + }; + + for (auto bannerObject : document.object()["banner"].toArray()) { + auto banner = Banner(); + banner.link = QUrl(bannerObject.toObject()["link"].toString()); + banner.bannerImage = QUrl(bannerObject.toObject()["lsb_banner"].toString()); + + headline->banners.push_back(banner); + } + + for (auto newsObject : document.object()["news"].toArray()) { + auto news = parseNews(newsObject.toObject()); + headline->news.push_back(news); + } + + for (auto pinnedObject : document.object()["pinned"].toArray()) { + auto pinned = parseNews(pinnedObject.toObject()); + headline->pinned.push_back(pinned); + } + + for (auto pinnedObject : document.object()["topics"].toArray()) { + auto pinned = parseNews(pinnedObject.toObject()); + headline->topics.push_back(pinned); + } + + m_headline = headline; + Q_EMIT newsChanged(); + }); +} + +Headline *LauncherCore::headline() +{ + return m_headline; +} + +bool LauncherCore::isSteam() const +{ + return m_isSteam; +} + +void LauncherCore::openOfficialLauncher(Profile *profile) +{ + struct Argument { + QString key, value; + }; + + QString executeArg("%1%2%3%4"); + QDateTime dateTime = QDateTime::currentDateTime(); + executeArg = executeArg.arg(dateTime.date().month() + 1, 2, 10, QLatin1Char('0')); + executeArg = executeArg.arg(dateTime.date().day(), 2, 10, QLatin1Char('0')); + 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)}); + + const QString argFormat = " /%1 =%2"; + + QString argJoined; + for (auto &arg : arguments) { + argJoined += argFormat.arg(arg.key, arg.value.replace(" ", " ")); + } + + QString finalArg = encryptGameArg(argJoined); + + auto launcherProcess = new QProcess(); + launchExecutable(*profile, launcherProcess, {profile->gamePath() + "/boot/ffxivlauncher64.exe", finalArg}, false, true); +} + +void LauncherCore::openSystemInfo(Profile *profile) +{ + auto sysinfoProcess = new QProcess(); + launchExecutable(*profile, sysinfoProcess, {profile->gamePath() + "/boot/ffxivsysinfo64.exe"}, false, false); +} + +void LauncherCore::openConfigBackup(Profile *profile) +{ + auto configProcess = new QProcess(); + launchExecutable(*profile, configProcess, {profile->gamePath() + "/boot/ffxivconfig64.exe"}, false, false); +} diff --git a/launcher/src/main.cpp b/launcher/src/main.cpp new file mode 100755 index 0000000..b617aa2 --- /dev/null +++ b/launcher/src/main.cpp @@ -0,0 +1,85 @@ +#include +#include +#include +#include +#include +#include + +#include "gameinstaller.h" +#include "launchercore.h" +#include "sapphirelauncher.h" + +int main(int argc, char *argv[]) +{ + QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); + + QApplication app(argc, argv); + // Default to org.kde.desktop style unless the user forces another style + if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) { + QQuickStyle::setStyle(QStringLiteral("org.kde.desktop")); + } + + KLocalizedString::setApplicationDomain("astra"); + QCoreApplication::setOrganizationDomain("xiv.zone"); + + KAboutData about(QStringLiteral("astra"), i18n("Astra"), "0.5.0", i18n("FFXIV Launcher"), KAboutLicense::GPL_V3, i18n("© 2023 Joshua Goins")); + about.addAuthor(i18n("Joshua Goins"), i18n("Maintainer"), QStringLiteral("josh@redstrate.com")); + about.setHomepage("https://xiv.zone/astra"); + about.addComponent("physis"); + about.setDesktopFileName("com.redstrate.astra.desktop"); // TODO: temporary + + KAboutData::setApplicationData(about); + + QCommandLineParser parser; + parser.setApplicationDescription(i18n("Linux FFXIV Launcher")); + +#ifdef ENABLE_STEAM + QCommandLineOption steamOption("steam", "Used for booting the launcher from Steam.", "verb"); + steamOption.setFlags(QCommandLineOption::HiddenFromHelp); + parser.addOption(steamOption); +#endif + + about.setupCommandLine(&parser); + parser.parse(QCoreApplication::arguments()); + about.processCommandLine(&parser); + +#ifdef ENABLE_STEAM + if (parser.isSet(steamOption)) { + const QStringList args = parser.positionalArguments(); + // Steam tries to use as a compatibiltiy tool, running install scripts (like DirectX), so try to ignore it. + if (!args[0].contains("ffxivboot.exe")) { + return 0; + } + } + + LauncherCore c(parser.isSet(steamOption)); +#else + LauncherCore c(false); +#endif + + qmlRegisterSingletonInstance("com.redstrate.astra", 1, 0, "LauncherCore", &c); + qmlRegisterUncreatableType("com.redstrate.astra", 1, 0, "GameInstaller", QStringLiteral("Use LauncherCore::createInstaller")); + qmlRegisterUncreatableType("com.redstrate.astra", 1, 0, "AccountManager", QStringLiteral("Use LauncherCore::accountManager")); + qmlRegisterUncreatableType("com.redstrate.astra", 1, 0, "ProfileManager", QStringLiteral("Use LauncherCore::profileManager")); + qmlRegisterUncreatableType("com.redstrate.astra", 1, 0, "Profile", QStringLiteral("Use from ProfileManager")); + qmlRegisterUncreatableType("com.redstrate.astra", 1, 0, "Account", QStringLiteral("Use from AccountManager")); + qmlRegisterSingletonType("com.redstrate.astra", 1, 0, "About", [](QQmlEngine *engine, QJSEngine *) -> QJSValue { + return engine->toScriptValue(KAboutData::applicationData()); + }); + qmlRegisterUncreatableType("com.redstrate.astra", 1, 0, "Headline", QStringLiteral("Use from AccountManager")); + qRegisterMetaType("Banner"); + qRegisterMetaType>("QList"); + qRegisterMetaType>("QList"); + + QQmlApplicationEngine engine; + engine.rootContext()->setContextObject(new KLocalizedContext(&engine)); + QObject::connect(&engine, &QQmlApplicationEngine::quit, &app, &QCoreApplication::quit); + + engine.load(QUrl(QStringLiteral("qrc:/ui/main.qml"))); + if (engine.rootObjects().isEmpty()) { + return -1; + } + + return QCoreApplication::exec(); +} diff --git a/launcher/core/src/patcher.cpp b/launcher/src/patcher.cpp similarity index 74% rename from launcher/core/src/patcher.cpp rename to launcher/src/patcher.cpp index 96d30b0..0106eb5 100644 --- a/launcher/core/src/patcher.cpp +++ b/launcher/src/patcher.cpp @@ -1,4 +1,5 @@ #include "patcher.h" + #include #include #include @@ -8,30 +9,39 @@ #include #include -Patcher::Patcher(QString baseDirectory, BootData* boot_data) : boot_data(boot_data), baseDirectory(std::move(baseDirectory)) { - dialog = new QProgressDialog(); +Patcher::Patcher(QString baseDirectory, BootData *boot_data, QObject *parent) + : QObject(parent) + , baseDirectory(std::move(baseDirectory)) + , boot_data(boot_data) +{ + /*dialog = new QProgressDialog(); dialog->setLabelText("Checking the FINAL FANTASY XIV Updater/Launcher version."); - dialog->show(); + dialog->show();*/ } -Patcher::Patcher(QString baseDirectory, GameData* game_data) : game_data(game_data), baseDirectory(std::move(baseDirectory)) { - dialog = new QProgressDialog(); +Patcher::Patcher(QString baseDirectory, GameData *game_data, QObject *parent) + : QObject(parent) + , baseDirectory(std::move(baseDirectory)) + , game_data(game_data) +{ + /*dialog = new QProgressDialog(); dialog->setLabelText("Checking the FINAL FANTASY XIV Game version."); - dialog->show(); + dialog->show();*/ } -void Patcher::processPatchList(QNetworkAccessManager& mgr, const QString& patchList) { +void Patcher::processPatchList(QNetworkAccessManager &mgr, const QString &patchList) +{ if (patchList.isEmpty()) { - dialog->hide(); + // dialog->hide(); emit done(); } else { if (isBoot()) { - dialog->setLabelText("Updating the FINAL FANTASY XIV Updater/Launcher version."); + // dialog->setLabelText("Updating the FINAL FANTASY XIV Updater/Launcher version."); } else { - dialog->setLabelText("Updating the FINAL FANTASY XIV Game version."); + // dialog->setLabelText("Updating the FINAL FANTASY XIV Game version."); } const QStringList parts = patchList.split("\r\n"); @@ -42,6 +52,7 @@ void Patcher::processPatchList(QNetworkAccessManager& mgr, const QString& patchL const QStringList patchParts = parts[i].split("\t"); const int length = patchParts[0].toInt(); + Q_UNUSED(length) QString name, url, version, repository; @@ -60,7 +71,7 @@ void Patcher::processPatchList(QNetworkAccessManager& mgr, const QString& patchL auto url_parts = url.split('/'); repository = url_parts[url_parts.size() - 3]; - if (isBoot()) { + /*if (isBoot()) { dialog->setLabelText( "Updating the FINAL FANTASY XIV Updater/Launcher version.\nDownloading ffxivboot - " + version); } else { @@ -69,10 +80,9 @@ void Patcher::processPatchList(QNetworkAccessManager& mgr, const QString& patchL } dialog->setMinimum(0); - dialog->setMaximum(length); + dialog->setMaximum(length);*/ - const QString patchesDir = - QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/patches/" + repository; + const QString patchesDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/patches/" + repository; if (!QDir().exists(patchesDir)) QDir().mkpath(patchesDir); @@ -83,7 +93,9 @@ void Patcher::processPatchList(QNetworkAccessManager& mgr, const QString& patchL QNetworkRequest patchRequest(url); auto patchReply = mgr.get(patchRequest); connect(patchReply, &QNetworkReply::downloadProgress, [=](int recieved, int total) { - dialog->setValue(recieved); + Q_UNUSED(recieved) + Q_UNUSED(total) + // dialog->setValue(recieved); }); connect(patchReply, &QNetworkReply::finished, [=] { @@ -111,21 +123,23 @@ void Patcher::processPatchList(QNetworkAccessManager& mgr, const QString& patchL } } -void Patcher::checkIfDone() { +void Patcher::checkIfDone() +{ if (remainingPatches <= 0) { - for (const auto& patch : patchQueue) { + for (const auto &patch : patchQueue) { processPatch(patch); } patchQueue.clear(); - dialog->hide(); + // dialog->hide(); emit done(); } } -void Patcher::processPatch(const QueuedPatch& patch) { +void Patcher::processPatch(const QueuedPatch &patch) +{ if (isBoot()) { physis_bootdata_apply_patch(boot_data, patch.path.toStdString().c_str()); } else { diff --git a/launcher/src/profile.cpp b/launcher/src/profile.cpp new file mode 100644 index 0000000..a855b2b --- /dev/null +++ b/launcher/src/profile.cpp @@ -0,0 +1,495 @@ +#include "profile.h" + +#include +#include +#include +#include + +#include "account.h" +#include "launchercore.h" + +Profile::Profile(LauncherCore &launcher, const QString &key, QObject *parent) + : QObject(parent) + , m_uuid(key) + , m_config(key) + , m_launcher(launcher) +{ + readGameVersion(); + + const QString dataDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + + const bool hasDalamud = QFile::exists(dataDir + "/Dalamud"); + if (hasDalamud) { + if (QFile::exists(dataDir + "/Dalamud/Dalamud.deps.json")) { + QFile depsJson(dataDir + "/Dalamud/Dalamud.deps.json"); + depsJson.open(QFile::ReadOnly); + QJsonDocument doc = QJsonDocument::fromJson(depsJson.readAll()); + + QString versionString; + if (doc["targets"].toObject().contains(".NETCoreApp,Version=v5.0")) { + versionString = doc["targets"].toObject()[".NETCoreApp,Version=v5.0"].toObject().keys().filter("Dalamud")[0]; + } else { + versionString = doc["targets"].toObject()[".NETCoreApp,Version=v6.0"].toObject().keys().filter("Dalamud")[0]; + } + + dalamudVersion = versionString.remove("Dalamud/"); + } + + if (QFile::exists(dataDir + "/DalamudAssets/asset.ver")) { + QFile assetJson(dataDir + "/DalamudAssets/asset.ver"); + assetJson.open(QFile::ReadOnly | QFile::Text); + + dalamudAssetVersion = QString(assetJson.readAll()).toInt(); + } + + if (QFile::exists(dataDir + "/DalamudRuntime/runtime.ver")) { + QFile runtimeVer(dataDir + "/DalamudRuntime/runtime.ver"); + runtimeVer.open(QFile::ReadOnly | QFile::Text); + + runtimeVersion = QString(runtimeVer.readAll()); + } + } +} + +void Profile::readGameData() +{ + physis_EXH *exh = physis_gamedata_read_excel_sheet_header(gameData, "ExVersion"); + if (exh != nullptr) { + physis_EXD exd = physis_gamedata_read_excel_sheet(gameData, "ExVersion", exh, Language::English, 0); + + for (unsigned int i = 0; i < exd.row_count; i++) { + expansionNames.push_back(exd.row_data[i].column_data[0].string._0); + } + + physis_gamedata_free_sheet(exd); + physis_gamedata_free_sheet_header(exh); + } +} + +void Profile::readWineInfo() +{ +#if defined(Q_OS_MAC) + switch (wineType) { + case WineType::System: // system wine + winePath = "/usr/local/bin/wine64"; + break; + case WineType::Custom: // custom path + winePath = profile.winePath; + break; + case WineType::Builtin: // ffxiv built-in (for mac users) + winePath = + "/Applications/FINAL FANTASY XIV " + "ONLINE.app/Contents/SharedSupport/finalfantasyxiv/FINAL FANTASY XIV ONLINE/wine"; + break; + case WineType::XIVOnMac: + winePath = "/Applications/XIV on Mac.app/Contents/Resources/wine/bin/wine64"; + break; + } +#endif + +#if defined(Q_OS_LINUX) + switch (wineType()) { + case WineType::System: // system wine (should be in $PATH) + setWinePath("/usr/bin/wine"); + break; + case WineType::Custom: // custom pth + break; + default: + break; + } +#endif + +#if defined(Q_OS_LINUX) || defined(Q_OS_MAC) + auto wineProcess = new QProcess(this); + wineProcess->setProcessChannelMode(QProcess::MergedChannels); + + connect(wineProcess, &QProcess::readyRead, this, [wineProcess, this] { + m_wineVersion = wineProcess->readAllStandardOutput().trimmed(); + Q_EMIT wineVersionText(); + }); + + m_launcher.launchExecutable(*this, wineProcess, {"--version"}, false, false); + + wineProcess->waitForFinished(); +#endif +} + +QString Profile::name() const +{ + return m_config.name(); +} + +void Profile::setName(const QString &name) +{ + if (m_config.name() != name) { + m_config.setName(name); + m_config.save(); + Q_EMIT nameChanged(); + } +} + +int Profile::language() const +{ + return m_config.language(); +} + +void Profile::setLanguage(const int value) +{ + if (m_config.language() != value) { + m_config.setLanguage(value); + m_config.save(); + Q_EMIT languageChanged(); + } +} + +QString Profile::gamePath() const +{ + return m_config.gamePath(); +} + +void Profile::setGamePath(const QString &path) +{ + if (m_config.gamePath() != path) { + m_config.setGamePath(path); + m_config.save(); + Q_EMIT gamePathChanged(); + } +} + +QString Profile::winePath() const +{ + return m_config.winePath(); +} + +void Profile::setWinePath(const QString &path) +{ + if (m_config.winePath() != path) { + m_config.setWinePath(path); + m_config.save(); + Q_EMIT winePathChanged(); + } +} + +QString Profile::winePrefixPath() const +{ + return m_config.winePrefixPath(); +} + +void Profile::setWinePrefixPath(const QString &path) +{ + if (m_config.winePrefixPath() != path) { + m_config.setWinePrefixPath(path); + m_config.save(); + Q_EMIT winePrefixPathChanged(); + } +} + +bool Profile::watchdogEnabled() const +{ + return m_config.enableWatchdog(); +} + +void Profile::setWatchdogEnabled(const bool value) +{ + if (m_config.enableWatchdog() != value) { + m_config.setEnableWatchdog(value); + m_config.save(); + Q_EMIT enableWatchdogChanged(); + } +} + +Profile::WineType Profile::wineType() const +{ + return static_cast(m_config.wineType()); +} + +void Profile::setWineType(const WineType type) +{ + if (static_cast(m_config.wineType()) != type) { + m_config.setWineType(static_cast(type)); + m_config.save(); + Q_EMIT wineTypeChanged(); + } +} + +bool Profile::esyncEnabled() const +{ + return m_config.useESync(); +} + +void Profile::setESyncEnabled(const bool value) +{ + if (m_config.useESync() != value) { + m_config.setUseESync(value); + m_config.save(); + Q_EMIT useESyncChanged(); + } +} + +bool Profile::gamescopeEnabled() const +{ + return m_config.useGamescope(); +} + +void Profile::setGamescopeEnabled(const bool value) +{ + if (m_config.useGamescope() != value) { + m_config.setUseGamescope(value); + m_config.save(); + Q_EMIT useGamescopeChanged(); + } +} + +bool Profile::gamemodeEnabled() const +{ + return m_config.useGamemode(); +} + +void Profile::setGamemodeEnabled(const bool value) +{ + if (m_config.useGamemode() != value) { + m_config.setUseGamemode(value); + m_config.save(); + Q_EMIT useGamemodeChanged(); + } +} + +bool Profile::directx9Enabled() const +{ + return m_config.useDX9(); +} + +void Profile::setDirectX9Enabled(const bool value) +{ + if (m_config.useDX9() != value) { + m_config.setUseDX9(value); + m_config.save(); + Q_EMIT useDX9Changed(); + } +} + +bool Profile::gamescopeFullscreen() const +{ + return m_config.gamescopeFullscreen(); +} + +void Profile::setGamescopeFullscreen(const bool value) +{ + if (m_config.gamescopeFullscreen() != value) { + m_config.setGamescopeFullscreen(value); + m_config.save(); + Q_EMIT gamescopeFullscreenChanged(); + } +} + +bool Profile::gamescopeBorderless() const +{ + return m_config.gamescopeBorderless(); +} + +void Profile::setGamescopeBorderless(const bool value) +{ + if (m_config.gamescopeBorderless() != value) { + m_config.setGamescopeBorderless(value); + m_config.save(); + Q_EMIT gamescopeBorderlessChanged(); + } +} + +int Profile::gamescopeWidth() const +{ + return m_config.gamescopeWidth(); +} + +void Profile::setGamescopeWidth(const int value) +{ + if (m_config.gamescopeWidth() != value) { + m_config.setGamescopeWidth(value); + m_config.save(); + Q_EMIT gamescopeWidthChanged(); + } +} + +int Profile::gamescopeHeight() const +{ + return m_config.gamescopeHeight(); +} + +void Profile::setGamescopeHeight(const int value) +{ + if (m_config.gamescopeHeight() != value) { + m_config.setGamescopeHeight(value); + m_config.save(); + Q_EMIT gamescopeHeightChanged(); + } +} + +int Profile::gamescopeRefreshRate() const +{ + return m_config.gamescopeRefreshRate(); +} + +void Profile::setGamescopeRefreshRate(const int value) +{ + if (m_config.gamescopeRefreshRate() != value) { + m_config.setGamescopeRefreshRate(value); + m_config.save(); + Q_EMIT gamescopeRefreshRateChanged(); + } +} + +bool Profile::dalamudEnabled() const +{ + return m_config.dalamudEnabled(); +} + +void Profile::setDalamudEnabled(const bool value) +{ + if (m_config.dalamudEnabled() != value) { + m_config.setDalamudEnabled(value); + m_config.save(); + Q_EMIT dalamudEnabledChanged(); + } +} + +bool Profile::dalamudOptOut() const +{ + return m_config.dalamudOptOut(); +} + +void Profile::setDalamudOptOut(const bool value) +{ + if (m_config.dalamudOptOut() != value) { + m_config.setDalamudOptOut(value); + m_config.save(); + Q_EMIT dalamudOptOutChanged(); + } +} + +Profile::DalamudChannel Profile::dalamudChannel() const +{ + return static_cast(m_config.dalamudChannel()); +} + +void Profile::setDalamudChannel(const DalamudChannel value) +{ + if (static_cast(m_config.dalamudChannel()) != value) { + m_config.setDalamudChannel(static_cast(value)); + m_config.save(); + Q_EMIT dalamudChannelChanged(); + } +} + +bool Profile::argumentsEncrypted() const +{ + return m_config.encryptArguments(); +} + +void Profile::setArgumentsEncrypted(const bool value) +{ + if (m_config.encryptArguments() != value) { + m_config.setEncryptArguments(value); + m_config.save(); + Q_EMIT encryptedArgumentsChanged(); + } +} + +Account *Profile::account() const +{ + return m_account; +} + +void Profile::setAccount(Account *account) +{ + if (account != m_account) { + m_account = account; + if (account->uuid() != m_config.account()) { + m_config.setAccount(account->uuid()); + m_config.save(); + } + Q_EMIT accountChanged(); + } +} + +void Profile::readGameVersion() +{ + if (gamePath().isEmpty()) { + return; + } + + gameData = physis_gamedata_initialize((gamePath() + "/game").toStdString().c_str()); + bootData = physis_bootdata_initialize((gamePath() + "/boot").toStdString().c_str()); + + if (bootData != nullptr) { + bootVersion = physis_bootdata_get_version(bootData); + } + + if (gameData != nullptr) { + repositories = physis_gamedata_get_repositories(gameData); + readGameData(); + } + + Q_EMIT gameInstallChanged(); +} + +QString Profile::accountUuid() const +{ + return m_config.account(); +} + +QString Profile::expansionVersionText() const +{ + if (!isGameInstalled()) { + return "No game installed."; + } else { + QString expacString; + + expacString += "Boot"; + expacString += QString(" (%1)").arg(bootVersion); + + for (unsigned int i = 0; i < repositories.repositories_count; i++) { + QString expansionName = "Unknown Expansion"; + if (i < static_cast(expansionNames.size())) { + expansionName = expansionNames[i]; + } + + expacString += QString("\n%1 (%2)").arg(expansionName, repositories.repositories[i].version); + } + + return expacString; + } +} + +QString Profile::dalamudVersionText() const +{ + QString text; + if (dalamudVersion.isEmpty()) { + text += "Dalamud is not installed."; + } else { + text += dalamudVersion; + } + + if (dalamudAssetVersion != -1) { + text += "\n" + QString::number(dalamudAssetVersion); + } + + return text; +} + +QString Profile::uuid() const +{ + return m_uuid; +} + +QString Profile::wineVersionText() const +{ + if (m_launcher.isSteam()) { + return "Wine is being managed by Steam."; + } + + if (!isWineInstalled()) { + return "Wine is not installed."; + } else { + return m_wineVersion; + } +} diff --git a/launcher/src/profilemanager.cpp b/launcher/src/profilemanager.cpp new file mode 100644 index 0000000..84b24b0 --- /dev/null +++ b/launcher/src/profilemanager.cpp @@ -0,0 +1,140 @@ +#include "profilemanager.h" + +#include + +ProfileManager::ProfileManager(LauncherCore &launcher, QObject *parent) + : QAbstractListModel(parent) + , m_launcher(launcher) +{ +} + +Profile *ProfileManager::getProfile(const int index) +{ + return m_profiles[index]; +} + +int ProfileManager::getProfileIndex(const QString &name) +{ + for (int i = 0; i < m_profiles.size(); i++) { + if (m_profiles[i]->name() == name) + return i; + } + + return -1; +} + +Profile *ProfileManager::addProfile() +{ + auto newProfile = new Profile(m_launcher, QUuid::createUuid().toString(), this); + newProfile->setName("New Profile"); + + newProfile->readWineInfo(); + + newProfile->setGamePath(getDefaultGamePath()); + newProfile->setWinePrefixPath(getDefaultWinePrefixPath()); + + insertProfile(newProfile); + + return newProfile; +} + +void ProfileManager::deleteProfile(Profile *profile) +{ + auto config = KSharedConfig::openStateConfig(); + config->deleteGroup(QString("profile-%1").arg(profile->uuid())); + config->sync(); + + const int row = m_profiles.indexOf(profile); + beginRemoveRows(QModelIndex(), row, row); + m_profiles.removeAll(profile); + endRemoveRows(); +} + +QString ProfileManager::getDefaultWinePrefixPath() +{ +#if defined(Q_OS_MACOS) + return QDir::homePath() + "/Library/Application Support/FINAL FANTASY XIV ONLINE/Bottles/published_Final_Fantasy"; +#endif + +#if defined(Q_OS_LINUX) + return QDir::homePath() + "/.wine"; +#endif + + return ""; +} + +QString ProfileManager::getDefaultGamePath() +{ +#if defined(Q_OS_WIN) + return "C:\\Program Files (x86)\\SquareEnix\\FINAL FANTASY XIV - A Realm Reborn"; +#endif + +#if defined(Q_OS_MAC) + return QDir::homePath() + + "/Library/Application Support/FINAL FANTASY XIV ONLINE/Bottles/published_Final_Fantasy/drive_c/Program " + "Files (x86)/SquareEnix/FINAL FANTASY XIV - A Realm Reborn"; +#endif + +#if defined(Q_OS_LINUX) + return QDir::homePath() + "/.wine/drive_c/Program Files (x86)/SquareEnix/FINAL FANTASY XIV - A Realm Reborn"; +#endif +} + +void ProfileManager::load() +{ + auto config = KSharedConfig::openStateConfig(); + for (const auto &id : config->groupList()) { + if (id.contains("profile-")) { + auto profile = new Profile(m_launcher, QString(id).remove("profile-"), this); + insertProfile(profile); + } + } + + // Add a dummy profile if none exist + if (m_profiles.empty()) { + addProfile(); + } +} + +int ProfileManager::rowCount(const QModelIndex &index) const +{ + Q_UNUSED(index); + return m_profiles.size(); +} + +QVariant ProfileManager::data(const QModelIndex &index, int role) const +{ + if (!checkIndex(index)) { + return {}; + } + + const int row = index.row(); + if (role == ProfileRole) { + return QVariant::fromValue(m_profiles[row]); + } + + return {}; +} + +QHash ProfileManager::roleNames() const +{ + return {{ProfileRole, QByteArrayLiteral("profile")}}; +} + +void ProfileManager::insertProfile(Profile *profile) +{ + beginInsertRows(QModelIndex(), m_profiles.size(), m_profiles.size()); + m_profiles.append(profile); + endInsertRows(); +} + +QVector ProfileManager::profiles() const +{ + return m_profiles; +} + +bool ProfileManager::canDelete(Profile *account) const +{ + Q_UNUSED(account) + return m_profiles.size() != 1; +} diff --git a/launcher/core/src/sapphirelauncher.cpp b/launcher/src/sapphirelauncher.cpp similarity index 79% rename from launcher/core/src/sapphirelauncher.cpp rename to launcher/src/sapphirelauncher.cpp index aa97595..0ced85c 100644 --- a/launcher/core/src/sapphirelauncher.cpp +++ b/launcher/src/sapphirelauncher.cpp @@ -2,12 +2,16 @@ #include #include -#include #include -SapphireLauncher::SapphireLauncher(LauncherCore& window) : window(window), QObject(&window) {} +SapphireLauncher::SapphireLauncher(LauncherCore &window, QObject *parent) + : QObject(parent) + , window(window) +{ +} -void SapphireLauncher::login(const QString& lobbyUrl, const LoginInformation& info) { +void SapphireLauncher::login(const QString &lobbyUrl, const LoginInformation &info) +{ QJsonObject data{{"username", info.username}, {"pass", info.password}}; QUrl url(lobbyUrl + "/sapphire-api/lobby/login"); @@ -25,16 +29,17 @@ void SapphireLauncher::login(const QString& lobbyUrl, const LoginInformation& in auth.frontierHost = document["frontierHost"].toString(); auth.region = 3; - window.launchGame(*info.settings, auth); + window.launchGame(*info.profile, auth); } else { - auto messageBox = + /*auto messageBox = new QMessageBox(QMessageBox::Icon::Critical, "Failed to Login", "Invalid username/password."); - messageBox->show(); + messageBox->show();*/ } }); } -void SapphireLauncher::registerAccount(const QString& lobbyUrl, const LoginInformation& info) { +void SapphireLauncher::registerAccount(const QString &lobbyUrl, const LoginInformation &info) +{ QJsonObject data{{"username", info.username}, {"pass", info.password}}; QUrl url(lobbyUrl + "/sapphire-api/lobby/createAccount"); @@ -51,6 +56,6 @@ void SapphireLauncher::registerAccount(const QString& lobbyUrl, const LoginInfor auth.frontierHost = document["frontierHost"].toString(); auth.region = 3; - window.launchGame(*info.settings, auth); + window.launchGame(*info.profile, auth); }); } \ No newline at end of file diff --git a/launcher/core/src/squareboot.cpp b/launcher/src/squareboot.cpp similarity index 67% rename from launcher/core/src/squareboot.cpp rename to launcher/src/squareboot.cpp index a13f0e8..377aba6 100644 --- a/launcher/core/src/squareboot.cpp +++ b/launcher/src/squareboot.cpp @@ -1,23 +1,30 @@ #include "squareboot.h" +#include #include #include -#include #include -#include #include #include #include +#include "account.h" #include "squarelauncher.h" -SquareBoot::SquareBoot(LauncherCore& window, SquareLauncher& launcher) - : window(window), launcher(launcher), QObject(&window) {} +SquareBoot::SquareBoot(LauncherCore &window, SquareLauncher &launcher, QObject *parent) + : QObject(parent) + , window(window) + , launcher(launcher) +{ +} -void SquareBoot::bootCheck(const LoginInformation& info) { - patcher = new Patcher(info.settings->gamePath + "/boot", info.settings->bootData); +void SquareBoot::bootCheck(const LoginInformation &info) +{ + Q_EMIT window.stageChanged(i18n("Checking for launcher updates...")); + + patcher = new Patcher(info.profile->gamePath() + "/boot", info.profile->bootData); connect(patcher, &Patcher::done, [=, &info] { - window.readGameVersion(); + info.profile->readGameVersion(); launcher.getStored(info); }); @@ -28,11 +35,11 @@ void SquareBoot::bootCheck(const LoginInformation& info) { QUrl url; url.setScheme("http"); url.setHost("patch-bootver.ffxiv.com"); - url.setPath(QString("/http/win32/ffxivneo_release_boot/%1").arg(info.settings->bootVersion)); + url.setPath(QString("/http/win32/ffxivneo_release_boot/%1").arg(info.profile->bootVersion)); url.setQuery(query); auto request = QNetworkRequest(url); - if (info.settings->license == GameLicense::macOS) { + if (info.profile->account()->license() == Account::GameLicense::macOS) { request.setRawHeader("User-Agent", "FFXIV-MAC PATCH CLIENT"); } else { request.setRawHeader("User-Agent", "FFXIV PATCH CLIENT"); @@ -41,21 +48,24 @@ void SquareBoot::bootCheck(const LoginInformation& info) { request.setRawHeader("Host", "patch-bootver.ffxiv.com"); auto reply = window.mgr->get(request); - connect(reply, &QNetworkReply::finished, [=, &info] { + connect(reply, &QNetworkReply::finished, [this, reply] { const QString response = reply->readAll(); patcher->processPatchList(*window.mgr, response); }); } -void SquareBoot::checkGateStatus(LoginInformation* info) { +void SquareBoot::checkGateStatus(LoginInformation *info) +{ + Q_EMIT window.stageChanged(i18n("Checking gate...")); + QUrl url("https://frontier.ffxiv.com/worldStatus/gate_status.json"); url.setQuery(QString::number(QDateTime::currentMSecsSinceEpoch())); QNetworkRequest request(url); // TODO: really? - window.buildRequest(*info->settings, request); + window.buildRequest(*info->profile, request); auto reply = window.mgr->get(request); connect(reply, &QNetworkReply::finished, [=] { @@ -64,8 +74,8 @@ void SquareBoot::checkGateStatus(LoginInformation* info) { // causing the launcher to be stuck in "maintenace mode". so if that happens, we try to rerun this logic. // TODO: this selection of errors is currently guesswork, i'm assuming one of these will fit the bill of // "internet is unavailable" in some way. - if (reply->error() == QNetworkReply::HostNotFoundError || reply->error() == QNetworkReply::TimeoutError || - reply->error() == QNetworkReply::UnknownServerError) + if (reply->error() == QNetworkReply::HostNotFoundError || reply->error() == QNetworkReply::TimeoutError + || reply->error() == QNetworkReply::UnknownServerError) checkGateStatus(info); QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); @@ -75,12 +85,7 @@ void SquareBoot::checkGateStatus(LoginInformation* info) { if (isGateOpen) { bootCheck(*info); } else { - auto messageBox = new QMessageBox( - QMessageBox::Icon::Critical, - "Failed to Login", - "The login gate is closed, the game may be under maintenance."); - - messageBox->show(); + Q_EMIT window.loginError(i18n("The login gate is closed, the game may be under maintenance.")); } }); } diff --git a/launcher/core/src/squarelauncher.cpp b/launcher/src/squarelauncher.cpp similarity index 53% rename from launcher/core/src/squarelauncher.cpp rename to launcher/src/squarelauncher.cpp index 161d5c8..53de690 100644 --- a/launcher/core/src/squarelauncher.cpp +++ b/launcher/src/squarelauncher.cpp @@ -1,18 +1,23 @@ #include "squarelauncher.h" +#include #include #include -#include #include -#include #include #include +#include "account.h" #include "launchercore.h" -SquareLauncher::SquareLauncher(LauncherCore& window) : window(window), QObject(&window) {} +SquareLauncher::SquareLauncher(LauncherCore &window, QObject *parent) + : QObject(parent) + , window(window) +{ +} -QString getFileHash(const QString& file) { +QString getFileHash(const QString &file) +{ auto f = QFile(file); if (!f.open(QIODevice::ReadOnly)) return ""; @@ -23,18 +28,21 @@ QString getFileHash(const QString& file) { return QString("%1/%2").arg(QString::number(f.size()), hash.result().toHex()); } -void SquareLauncher::getStored(const LoginInformation& info) { +void SquareLauncher::getStored(const LoginInformation &info) +{ + Q_EMIT window.stageChanged(i18n("Logging in...")); + QUrlQuery query; // en is always used to the top url query.addQueryItem("lng", "en"); // for some reason, we always use region 3. the actual region is acquired later query.addQueryItem("rgn", "3"); - query.addQueryItem("isft", info.settings->isFreeTrial ? "1" : "0"); + query.addQueryItem("isft", info.profile->account()->isFreeTrial() ? "1" : "0"); query.addQueryItem("cssmode", "1"); query.addQueryItem("isnew", "1"); query.addQueryItem("launchver", "3"); - if (info.settings->license == GameLicense::WindowsSteam) { + if (info.profile->account()->license() == Account::GameLicense::WindowsSteam) { query.addQueryItem("issteam", "1"); // TODO: get steam ticket information from steam api @@ -46,26 +54,22 @@ void SquareLauncher::getStored(const LoginInformation& info) { url.setQuery(query); auto request = QNetworkRequest(url); - window.buildRequest(*info.settings, request); + window.buildRequest(*info.profile, request); - QNetworkReply* reply = window.mgr->get(request); + QNetworkReply *reply = window.mgr->get(request); connect(reply, &QNetworkReply::finished, [=, &info] { auto str = QString(reply->readAll()); // fetches Steam username - if (info.settings->license == GameLicense::WindowsSteam) { + if (info.profile->account()->license() == Account::GameLicense::WindowsSteam) { QRegularExpression re(R"lit(.*)""\/>)lit"); QRegularExpressionMatch match = re.match(str); if (match.hasMatch()) { username = match.captured(1); } else { - auto messageBox = new QMessageBox( - QMessageBox::Icon::Critical, - "Failed to Login", - "Could not get Steam username, have you attached your account?"); - messageBox->show(); + Q_EMIT window.loginError(i18n("Could not get Steam username, have you attached your account?")); } } else { username = info.username; @@ -77,16 +81,14 @@ void SquareLauncher::getStored(const LoginInformation& info) { stored = match.captured(1); login(info, url); } else { - auto messageBox = new QMessageBox( - QMessageBox::Icon::Critical, - "Failed to Login", - "Failed to contact SE servers. They may be in maintenance."); - messageBox->show(); + Q_EMIT window.loginError( + i18n("Square Enix servers refused to confirm session information. The game may be under maintenance, try the official launcher.")); } }); } -void SquareLauncher::login(const LoginInformation& info, const QUrl& referer) { +void SquareLauncher::login(const LoginInformation &info, const QUrl &referer) +{ QUrlQuery postData; postData.addQueryItem("_STORED_", stored); postData.addQueryItem("sqexid", info.username); @@ -94,7 +96,7 @@ void SquareLauncher::login(const LoginInformation& info, const QUrl& referer) { postData.addQueryItem("otppw", info.oneTimePassword); QNetworkRequest request(QUrl("https://ffxiv-login.square-enix.com/oauth/ffxivarr/login/login.send")); - window.buildRequest(*info.settings, request); + window.buildRequest(*info.profile, request); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); request.setRawHeader("Referer", referer.toEncoded()); request.setRawHeader("Cache-Control", "no-cache"); @@ -112,32 +114,12 @@ void SquareLauncher::login(const LoginInformation& info, const QUrl& referer) { const bool playable = parts[9] == "1"; if (!playable) { - auto messageBox = new QMessageBox( - QMessageBox::Icon::Critical, - "Failed to Login", - "Your game is unplayable. Please check that you have the right license selected, and a " - "subscription to play."); - - auto launcherButton = messageBox->addButton("Open Mog Station", QMessageBox::HelpRole); - connect(launcherButton, &QPushButton::clicked, [=] { - QDesktopServices::openUrl(QUrl("https://sqex.to/Msp")); - }); - - messageBox->addButton(QMessageBox::StandardButton::Ok); - - messageBox->show(); - + Q_EMIT window.loginError(i18n("Your account is unplayable. Check that you have the correct license, and a valid subscription.")); return; } if (!terms) { - auto messageBox = new QMessageBox( - QMessageBox::Icon::Critical, - "Failed to Login", - "Your game is unplayable. You need to accept the terms of service from the official launcher."); - - messageBox->show(); - + Q_EMIT window.loginError(i18n("Your account is unplayable. You need to accept the terms of service from the official launcher first.")); return; } @@ -155,18 +137,17 @@ void SquareLauncher::login(const LoginInformation& info, const QUrl& referer) { // there's a stray quote at the end of the error string, so let's remove that QString errorStr = match.captured(1).chopped(1); - auto messageBox = new QMessageBox(QMessageBox::Icon::Critical, "Failed to Login", errorStr); - messageBox->show(); + Q_EMIT window.loginError(errorStr); } }); } -void SquareLauncher::registerSession(const LoginInformation& info) { +void SquareLauncher::registerSession(const LoginInformation &info) +{ QUrl url; url.setScheme("https"); url.setHost("patch-gamever.ffxiv.com"); - url.setPath(QString("/http/win32/ffxivneo_release_game/%1/%2") - .arg(info.settings->repositories.repositories[0].version, SID)); + url.setPath(QString("/http/win32/ffxivneo_release_game/%1/%2").arg(info.profile->repositories.repositories[0].version, SID)); auto request = QNetworkRequest(url); window.setSSL(request); @@ -174,12 +155,11 @@ void SquareLauncher::registerSession(const LoginInformation& info) { request.setRawHeader("User-Agent", "FFXIV PATCH CLIENT"); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - QString report = QString("%1=%2").arg(info.settings->bootVersion, getBootHash(info)); + QString report = QString("%1=%2").arg(info.profile->bootVersion, getBootHash(info)); for (int i = 1; i < auth.maxExpansion + 1; i++) { - if (i <= info.settings->repositories.repositories_count) { - report += - QString("\nex%1\t%2").arg(QString::number(i), info.settings->repositories.repositories[i].version); + if (i <= static_cast(info.profile->repositories.repositories_count)) { + report += QString("\nex%1\t%2").arg(QString::number(i), info.profile->repositories.repositories[i].version); } else { report += QString("\nex%1\t2012.01.01.0000.0000").arg(QString::number(i)); } @@ -191,60 +171,39 @@ void SquareLauncher::registerSession(const LoginInformation& info) { if (reply->rawHeaderList().contains("X-Patch-Unique-Id")) { QString body = reply->readAll(); - patcher = new Patcher(info.settings->gamePath + "/game", info.settings->gameData); + patcher = new Patcher(info.profile->gamePath() + "/game", info.profile->gameData); connect(patcher, &Patcher::done, [=, &info] { - window.readGameVersion(); + info.profile->readGameVersion(); auth.SID = reply->rawHeader("X-Patch-Unique-Id"); - window.launchGame(*info.settings, auth); + window.launchGame(*info.profile, auth); }); patcher->processPatchList(*window.mgr, body); } else { - auto messageBox = new QMessageBox( - QMessageBox::Icon::Critical, - "Failed to Login", - "Fatal error, request was successful but X-Patch-Unique-Id was not received"); - messageBox->show(); + Q_EMIT window.loginError(i18n("Fatal error, request was successful but X-Patch-Unique-Id was not recieved.")); } } else { if (reply->error() == QNetworkReply::SslHandshakeFailedError) { - auto messageBox = new QMessageBox( - QMessageBox::Icon::Critical, - "Failed to Login", - "SSL handshake error detected. If you are using OpenSUSE Tumbleweed or Fedora, this launcher will " - "only work if you run the following command `update-crypto-policies --set LEGACY`"); - messageBox->show(); + Q_EMIT window.loginError( + i18n("SSL handshake error detected. If you are using OpenSUSE or Fedora, try running `update-crypto-policies --set LEGACY`.")); } else if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 405) { - auto messageBox = new QMessageBox( - QMessageBox::Icon::Critical, - "Failed to Login", - "Failed the anti-tamper check. Please restore your game to the original state or update the " - "game."); - messageBox->show(); + Q_EMIT window.loginError(i18n("The game failed the anti-tamper check. Restore the game to the original state and try updating again.")); } else { - int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - auto messageBox = new QMessageBox( - QMessageBox::Icon::Critical, "Failed to Login", &"Unknown error! Status code was "[statusCode]); - messageBox->show(); + Q_EMIT window.loginError(i18n("Unknown error when registering the session.")); } } }); } -QString SquareLauncher::getBootHash(const LoginInformation& info) { - const QList fileList = { - "ffxivboot.exe", - "ffxivboot64.exe", - "ffxivlauncher.exe", - "ffxivlauncher64.exe", - "ffxivupdater.exe", - "ffxivupdater64.exe"}; +QString SquareLauncher::getBootHash(const LoginInformation &info) +{ + const QList fileList = {"ffxivboot.exe", "ffxivboot64.exe", "ffxivlauncher.exe", "ffxivlauncher64.exe", "ffxivupdater.exe", "ffxivupdater64.exe"}; QString result; for (int i = 0; i < fileList.count(); i++) { - result += fileList[i] + "/" + getFileHash(info.settings->gamePath + "/boot/" + fileList[i]); + result += fileList[i] + "/" + getFileHash(info.profile->gamePath() + "/boot/" + fileList[i]); if (i != fileList.length() - 1) result += ","; diff --git a/launcher/core/src/steamapi.cpp b/launcher/src/steamapi.cpp similarity index 64% rename from launcher/core/src/steamapi.cpp rename to launcher/src/steamapi.cpp index 0b6569e..6e8fb4e 100644 --- a/launcher/core/src/steamapi.cpp +++ b/launcher/src/steamapi.cpp @@ -1,33 +1,39 @@ #include "steamapi.h" -#include "launchercore.h" #ifdef ENABLE_STEAM #include #endif -SteamAPI::SteamAPI(LauncherCore& core) : core(core) { +#include "launchercore.h" + +SteamAPI::SteamAPI(LauncherCore &core, QObject *parent) + : QObject(parent) + , core(core) +{ #ifdef ENABLE_STEAM - if(core.isSteam) { + if (core.isSteam()) { qputenv("SteamAppId", "39210"); qputenv("SteamGameId", "39210"); - if(!SteamAPI_Init()) + if (!SteamAPI_Init()) qDebug() << "Failed to initialize steam api!"; } #endif } -void SteamAPI::setLauncherMode(bool isLauncher) { +void SteamAPI::setLauncherMode(bool isLauncher) +{ #ifdef ENABLE_STEAM - if(core.isSteam) { + if (core.isSteam()) { SteamUtils()->SetGameLauncherMode(isLauncher); } #endif } -bool SteamAPI::isDeck() const { +bool SteamAPI::isDeck() const +{ #ifdef ENABLE_STEAM - if(core.isSteam) { + if (core.isSteam()) { return SteamUtils()->IsSteamRunningOnSteamDeck(); } else { return false; diff --git a/launcher/core/src/watchdog.cpp b/launcher/src/watchdog.cpp similarity index 61% rename from launcher/core/src/watchdog.cpp rename to launcher/src/watchdog.cpp index b039fb1..512cc65 100644 --- a/launcher/core/src/watchdog.cpp +++ b/launcher/src/watchdog.cpp @@ -13,33 +13,32 @@ // from https://github.com/adobe/webkit/blob/master/Source/WebCore/plugins/qt/QtX11ImageConversion.cpp // code is licensed under GPLv2 // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies) -QImage qimageFromXImage(XImage* xi) { +QImage qimageFromXImage(XImage *xi) +{ QImage::Format format = QImage::Format_ARGB32_Premultiplied; if (xi->depth == 24) format = QImage::Format_RGB32; else if (xi->depth == 16) format = QImage::Format_RGB16; - QImage image = QImage(reinterpret_cast(xi->data), xi->width, xi->height, xi->bytes_per_line, format).copy(); + QImage image = QImage(reinterpret_cast(xi->data), xi->width, xi->height, xi->bytes_per_line, format).copy(); // we may have to swap the byte order - if ((QSysInfo::ByteOrder == QSysInfo::LittleEndian && xi->byte_order == MSBFirst) || - (QSysInfo::ByteOrder == QSysInfo::BigEndian && xi->byte_order == LSBFirst)) { - + if ((QSysInfo::ByteOrder == QSysInfo::LittleEndian && xi->byte_order == MSBFirst) + || (QSysInfo::ByteOrder == QSysInfo::BigEndian && xi->byte_order == LSBFirst)) { for (int i = 0; i < image.height(); i++) { if (xi->depth == 16) { - ushort* p = reinterpret_cast(image.scanLine(i)); - ushort* end = p + image.width(); + ushort *p = reinterpret_cast(image.scanLine(i)); + ushort *end = p + image.width(); while (p < end) { *p = ((*p << 8) & 0xff00) | ((*p >> 8) & 0x00ff); p++; } } else { - uint* p = reinterpret_cast(image.scanLine(i)); - uint* end = p + image.width(); + uint *p = reinterpret_cast(image.scanLine(i)); + uint *end = p + image.width(); while (p < end) { - *p = ((*p << 24) & 0xff000000) | ((*p << 8) & 0x00ff0000) | ((*p >> 8) & 0x0000ff00) | - ((*p >> 24) & 0x000000ff); + *p = ((*p << 24) & 0xff000000) | ((*p << 8) & 0x00ff0000) | ((*p >> 8) & 0x0000ff00) | ((*p >> 24) & 0x000000ff); p++; } } @@ -48,7 +47,7 @@ QImage qimageFromXImage(XImage* xi) { // fix-up alpha channel if (format == QImage::Format_RGB32) { - QRgb* p = reinterpret_cast(image.bits()); + QRgb *p = reinterpret_cast(image.bits()); for (int y = 0; y < xi->height; ++y) { for (int x = 0; x < xi->width; ++x) p[x] |= 0xff000000; @@ -59,7 +58,8 @@ QImage qimageFromXImage(XImage* xi) { return image; } -void Watchdog::launchGame(const ProfileSettings& settings, const LoginAuth& auth) { +void Watchdog::launchGame(const ProfileSettings &settings, const LoginAuth &auth) +{ if (icon == nullptr) { icon = new QSystemTrayIcon(); } @@ -91,20 +91,17 @@ void Watchdog::launchGame(const ProfileSettings& settings, const LoginAuth& auth if (processWindowId == -1) { auto xdoProcess = new QProcess(); - connect( - xdoProcess, - static_cast(&QProcess::finished), - [=](int, QProcess::ExitStatus) { - QString output = xdoProcess->readAllStandardOutput(); - qDebug() << "Found XIV Window: " << output.toInt(); + connect(xdoProcess, static_cast(&QProcess::finished), [=](int, QProcess::ExitStatus) { + QString output = xdoProcess->readAllStandardOutput(); + qDebug() << "Found XIV Window: " << output.toInt(); - processWindowId = output.toInt(); - }); + processWindowId = output.toInt(); + }); // TODO: don't use xdotool for this, find a better way to xdoProcess->start("bash", {"-c", "xdotool search --name \"FINAL FANTASY XIV\""}); } else { - Display* display = XOpenDisplay(nullptr); + Display *display = XOpenDisplay(nullptr); XSynchronize(display, True); @@ -119,7 +116,7 @@ void Watchdog::launchGame(const ProfileSettings& settings, const LoginAuth& auth XCompositeRedirectWindow(display, processWindowId, CompositeRedirectAutomatic); XCompositeNameWindowPixmap(display, processWindowId); - XRenderPictFormat* format = XRenderFindVisualFormat(display, attr.visual); + XRenderPictFormat *format = XRenderFindVisualFormat(display, attr.visual); XRenderPictureAttributes pa; pa.subwindow_mode = IncludeInferiors; @@ -127,7 +124,7 @@ void Watchdog::launchGame(const ProfileSettings& settings, const LoginAuth& auth Picture picture = XRenderCreatePicture(display, processWindowId, format, CPSubwindowMode, &pa); XFlush(display); // TODO: does this actually make a difference? - XImage* image = XGetImage(display, processWindowId, 0, 0, attr.width, attr.height, AllPlanes, ZPixmap); + XImage *image = XGetImage(display, processWindowId, 0, 0, attr.width, attr.height, AllPlanes, ZPixmap); if (!image) { qDebug() << "Unable to get image..."; } else { @@ -138,26 +135,24 @@ void Watchdog::launchGame(const ProfileSettings& settings, const LoginAuth& auth return; switch (result.state) { - case ScreenState::InLoginQueue: { - icon->showMessage( - "Watchdog", - QString("You are now at position %1 (moved %2 spots)") - .arg(result.playersInQueue) - .arg(lastResult.playersInQueue - result.playersInQueue)); + case ScreenState::InLoginQueue: { + icon->showMessage("Watchdog", + QString("You are now at position %1 (moved %2 spots)") + .arg(result.playersInQueue) + .arg(lastResult.playersInQueue - result.playersInQueue)); - icon->setToolTip(QString("Queue Status (%1)").arg(result.playersInQueue)); - } break; - case ScreenState::LobbyError: { - // TODO: kill game? - icon->showMessage("Watchdog", "You have been disconnected due to a lobby error."); - } break; - case ScreenState::ConnectingToDataCenter: { - icon->showMessage( - "Watchdog", "You are in the process of being connected to the data center."); - } break; - case ScreenState::WorldFull: { - icon->showMessage("Watchdog", "You have been disconnected due to a lobby error."); - } break; + icon->setToolTip(QString("Queue Status (%1)").arg(result.playersInQueue)); + } break; + case ScreenState::LobbyError: { + // TODO: kill game? + icon->showMessage("Watchdog", "You have been disconnected due to a lobby error."); + } break; + case ScreenState::ConnectingToDataCenter: { + icon->showMessage("Watchdog", "You are in the process of being connected to the data center."); + } break; + case ScreenState::WorldFull: { + icon->showMessage("Watchdog", "You have been disconnected due to a lobby error."); + } break; } lastResult = result; diff --git a/launcher/ui/Components/FormFileDelegate.qml b/launcher/ui/Components/FormFileDelegate.qml new file mode 100644 index 0000000..357ca74 --- /dev/null +++ b/launcher/ui/Components/FormFileDelegate.qml @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2023 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Window 2.15 +import org.kde.kirigami 2.20 as Kirigami +import QtQuick.Controls 2.15 as Controls +import QtQuick.Layouts 1.15 +import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm +import QtQuick.Dialogs 1.0 +import com.redstrate.astra 1.0 + +MobileForm.FormButtonDelegate { + id: control + + property string file + + icon.name: "document-open" + description: file + + onClicked: dialog.open() + + FileDialog { + id: dialog + + selectFolder: true + + folder: shortcuts.home + } +} \ No newline at end of file diff --git a/launcher/ui/Components/FormFolderDelegate.qml b/launcher/ui/Components/FormFolderDelegate.qml new file mode 100644 index 0000000..01ce1dd --- /dev/null +++ b/launcher/ui/Components/FormFolderDelegate.qml @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2023 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Window 2.15 +import org.kde.kirigami 2.20 as Kirigami +import QtQuick.Controls 2.15 as Controls +import QtQuick.Layouts 1.15 +import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm +import QtQuick.Dialogs 1.0 +import com.redstrate.astra 1.0 + +MobileForm.FormButtonDelegate { + id: control + + property string folder + + icon.name: "document-open-folder" + description: folder + + onClicked: dialog.open() + + FileDialog { + id: dialog + + selectFolder: true + + folder: shortcuts.home + } +} \ No newline at end of file diff --git a/launcher/ui/Pages/LoginPage.qml b/launcher/ui/Pages/LoginPage.qml new file mode 100644 index 0000000..b6e5a35 --- /dev/null +++ b/launcher/ui/Pages/LoginPage.qml @@ -0,0 +1,216 @@ +// SPDX-FileCopyrightText: 2023 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Window 2.15 +import org.kde.kirigami 2.20 as Kirigami +import QtQuick.Controls 2.15 as Controls +import QtQuick.Layouts 1.15 +import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm +import com.redstrate.astra 1.0 + +Kirigami.OverlayDrawer { + id: page + + property var profile: LauncherCore.profileManager.getProfile(0) + readonly property bool isLoginValid: { + if (!profile.account) { + return false + } + + if (usernameField.text.length === 0) { + return false + } + + if (!profile.account.rememberPassword && passwordField.text.length === 0) { + return false + } + + if (profile.account.useOTP && !profile.account.rememberOTP && otpField.text.length === 0) { + return false + } + + return true; + } + + function updateFields() { + usernameField.text = profile.account.name + passwordField.text = profile.account.rememberPassword ? profile.account.getPassword() : "" + otpField.text = "" + } + + Connections { + target: profile + + function onAccountChanged() { + updateFields() + } + } + + onProfileChanged: updateFields() + + ColumnLayout { + width: parent.width + MobileForm.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormButtonDelegate { + text: page.profile.name + + Controls.Menu { + id: profileMenu + + Repeater { + model: LauncherCore.profileManager + + Controls.MenuItem { + required property var profile + + Controls.MenuItem { + text: profile.name + + onClicked: { + page.profile = profile + profileMenu.close() + } + } + } + } + } + + onClicked: profileMenu.popup() + } + } + } + + MobileForm.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormButtonDelegate { + text: page.profile.account.name + + leading: Kirigami.Avatar + { + source: page.profile.account.avatarUrl + } + + leadingPadding: Kirigami.Units.largeSpacing * 2 + + Controls.Menu { + id: accountMenu + + Repeater { + model: LauncherCore.accountManager + + Controls.MenuItem { + required property var account + + Controls.MenuItem { + text: account.name + icon.name: account.avatarUrl.length === 0 ? "actor" : "" + icon.source: account.avatarUrl + + onClicked: { + page.profile.account = account + accountMenu.close() + } + } + } + } + } + + onClicked: accountMenu.popup() + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormTextFieldDelegate { + id: usernameField + label: i18n("Username") + text: page.profile.account.name + enabled: false + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormTextFieldDelegate { + id: passwordField + label: i18n("Password") + echoMode: TextInput.Password + focus: true + onAccepted: otpField.clicked() + text: page.profile.account.rememberPassword ? "abcdefg" : "" + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormTextFieldDelegate { + id: otpField + label: i18n("One-time password") + visible: page.profile.account.useOTP + onAccepted: loginButton.clicked() + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormButtonDelegate { + id: loginButton + + text: i18n("Log In") + icon.name: "unlock" + enabled: page.isLoginValid + onClicked: { + LauncherCore.login(page.profile, usernameField.text, passwordField.text, otpField.text) + pageStack.layers.push('qrc:/ui/Pages/StatusPage.qml') + } + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormButtonDelegate { + text: i18n("Settings") + icon.name: "configure" + onClicked: pageStack.pushDialogLayer('qrc:/ui/Settings/SettingsPage.qml') + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormButtonDelegate { + text: i18n("Open Official Launcher") + icon.name: "application-x-executable" + onClicked: LauncherCore.openOfficialLauncher(page.profile) + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormButtonDelegate { + text: i18n("Open System Info") + icon.name: "application-x-executable" + onClicked: LauncherCore.openSystemInfo(page.profile) + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormButtonDelegate { + text: i18n("Open Config Backup") + icon.name: "application-x-executable" + onClicked: LauncherCore.openConfigBackup(page.profile) + } + } + } + } +} \ No newline at end of file diff --git a/launcher/ui/Pages/NewsPage.qml b/launcher/ui/Pages/NewsPage.qml new file mode 100644 index 0000000..dca66ca --- /dev/null +++ b/launcher/ui/Pages/NewsPage.qml @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: 2023 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Window 2.15 +import org.kde.kirigami 2.20 as Kirigami +import QtQuick.Controls 2.15 as Controls +import QtQuick.Layouts 1.15 +import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm +import com.redstrate.astra 1.0 + +Kirigami.ScrollablePage { + id: page + + globalToolBarStyle: Kirigami.ApplicationHeaderStyle.None + + Component.onCompleted: LauncherCore.refreshNews() + + property int currentBannerIndex: 0 + property int numBannerImages: 0 + + Connections { + target: LauncherCore + + function onNewsChanged() { + page.currentBannerIndex = 0 + page.numBannerImages = LauncherCore.headline.banners.length + console.log(LauncherCore.headline.banners) + } + } + + Timer { + interval: 10000 + running: true + repeat: true + onTriggered: { + if (page.currentBannerIndex + 1 === page.numBannerImages) { + page.currentBannerIndex = 0 + } else { + page.currentBannerIndex++ + } + } + } + + ColumnLayout { + width: parent.width + MobileForm.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormCardHeader { + title: i18n("Banner") + } + + Image { + Layout.fillWidth: true + + source: LauncherCore.headline !== null ? LauncherCore.headline.banners[page.currentBannerIndex].bannerImage : "" + + fillMode: Image.PreserveAspectFit + + MouseArea { + anchors.fill: parent + + cursorShape: Qt.PointingHandCursor + + onClicked: Qt.openUrlExternally(LauncherCore.headline.banners[page.currentBannerIndex].link) + } + } + } + } + + MobileForm.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormCardHeader { + title: i18n("News") + } + + Repeater { + model: LauncherCore.headline.news + + MobileForm.FormButtonDelegate { + text: modelData.title + description: Qt.formatDate(modelData.date) + + onClicked: Qt.openUrlExternally(modelData.url) + } + } + } + } + + MobileForm.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormCardHeader { + title: i18n("Topics") + } + + Repeater { + model: LauncherCore.headline.topics + + MobileForm.FormButtonDelegate { + text: modelData.title + description: Qt.formatDate(modelData.date) + + onClicked: Qt.openUrlExternally(modelData.url) + } + } + } + } + } +} \ No newline at end of file diff --git a/launcher/ui/Pages/StatusPage.qml b/launcher/ui/Pages/StatusPage.qml new file mode 100644 index 0000000..5c1360d --- /dev/null +++ b/launcher/ui/Pages/StatusPage.qml @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2023 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Window 2.15 +import org.kde.kirigami 2.20 as Kirigami +import QtQuick.Controls 2.15 as Controls +import QtQuick.Layouts 1.15 +import com.redstrate.astra 1.0 + +Kirigami.Page { + property var gameInstaller + + title: i18n("Logging in...") + + Kirigami.LoadingPlaceholder { + id: placeholder + + anchors.centerIn: parent + } + + Kirigami.PromptDialog { + id: errorDialog + title: i18n("Login error") + + showCloseButton: false + standardButtons: Kirigami.Dialog.Ok + + onAccepted: applicationWindow().pageStack.layers.pop() + onRejected: applicationWindow().pageStack.layers.pop() + } + + Connections { + target: LauncherCore + + function onStageChanged(message) { + placeholder.text = message + } + + function onLoginError(message) { + errorDialog.subtitle = message + errorDialog.open() + } + } +} \ No newline at end of file diff --git a/launcher/ui/Settings/AccountSettings.qml b/launcher/ui/Settings/AccountSettings.qml new file mode 100644 index 0000000..6fd2dec --- /dev/null +++ b/launcher/ui/Settings/AccountSettings.qml @@ -0,0 +1,200 @@ +// SPDX-FileCopyrightText: 2023 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Window 2.15 +import org.kde.kirigami 2.20 as Kirigami +import QtQuick.Controls 2.15 as Controls +import QtQuick.Layouts 1.15 +import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm +import com.redstrate.astra 1.0 + +Kirigami.ScrollablePage { + id: page + + property var account + + title: i18n("Account Settings") + + ColumnLayout { + width: parent.width + + MobileForm.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormCardHeader { + title: i18n("General") + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormTextFieldDelegate { + label: i18n("Username") + text: page.account.name + onTextChanged: page.account.name = text + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormComboBoxDelegate { + text: i18n("Account type") + model: ["Square Enix", "Sapphire"] + currentIndex: page.account.isSapphire ? 1 : 0 + onCurrentIndexChanged: page.account.isSapphire = (currentIndex === 1) + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormComboBoxDelegate { + id: licenseField + text: i18n("License") + description: i18n("If the account holds multiple licenses, choose the preferred one.") + model: ["Windows", "Steam", "macOS"] + currentIndex: page.account.license + onCurrentIndexChanged: page.account.license = currentIndex + visible: !page.account.isSapphire + } + + MobileForm.FormDelegateSeparator { + visible: licenseField.visible + } + + MobileForm.FormCheckDelegate { + id: freeTrialField + text: i18n("Free trial") + checked: page.account.isFreeTrial + onCheckedChanged: page.account.isFreeTrial = checked + visible: !page.account.isSapphire + } + + MobileForm.FormDelegateSeparator { + visible: freeTrialField.visible + } + + MobileForm.FormCheckDelegate { + text: i18n("Needs a one-time password") + checked: page.account.useOTP + onCheckedChanged: page.account.useOTP = checked + visible: !page.account.isSapphire + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormButtonDelegate { + text: i18n("Set Lodestone Character") + description: i18n("Associate a character's avatar with this account.") + icon.name: "actor" + visible: !page.account.isSapphire + Kirigami.PromptDialog { + id: lodestoneDialog + title: i18n("Enter Lodestone Id") + + standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel + + onAccepted: page.account.lodestoneId = lodestoneIdField.text + + Controls.TextField { + id: lodestoneIdField + text: page.account.lodestoneId + placeholderText: qsTr("123456...") + } + } + + onClicked: lodestoneDialog.open() + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormTextFieldDelegate { + label: i18n("Lobby URL") + text: page.account.lobbyUrl + onTextChanged: page.account.lobbyUrl = text + visible: page.account.isSapphire + placeholderText: "neolobby0X.ffxiv.com" + } + } + } + + MobileForm.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormCardHeader { + title: i18n("Login") + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormCheckDelegate { + text: i18n("Remember password") + checked: page.account.rememberPassword + onCheckedChanged: page.account.rememberPassword = checked + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormCheckDelegate { + text: i18n("Automatically generate one-time passwords") + checked: page.account.rememberOTP + onCheckedChanged: page.account.rememberOTP = checked + enabled: page.account.useOTP + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormButtonDelegate { + text: i18n("Enter OTP Secret") + icon.name: "list-add-symbolic" + enabled: page.account.rememberOTP + Kirigami.PromptDialog { + id: otpDialog + title: i18n("Enter OTP Secret") + + standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel + + onAccepted: page.account.setOTPSecret(otpSecretField.text) + + Controls.TextField { + id: otpSecretField + placeholderText: qsTr("ABCD EFGH...") + } + } + + onClicked: otpDialog.open() + } + } + } + + MobileForm.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormButtonDelegate { + text: i18n("Delete Account") + description: !enabled ? i18n("Cannot delete the only account") : "" + icon.name: "delete" + enabled: LauncherCore.accountManager.canDelete(page.account) + onClicked: { + LauncherCore.accountManager.deleteAccount(page.account) + applicationWindow().pageStack.layers.pop() + } + } + } + } + } +} \ No newline at end of file diff --git a/launcher/ui/Settings/GeneralSettings.qml b/launcher/ui/Settings/GeneralSettings.qml new file mode 100644 index 0000000..b7d705d --- /dev/null +++ b/launcher/ui/Settings/GeneralSettings.qml @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2023 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Window 2.15 +import org.kde.kirigami 2.20 as Kirigami +import QtQuick.Controls 2.15 as Controls +import QtQuick.Layouts 1.15 +import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm +import com.redstrate.astra 1.0 + +MobileForm.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormCardHeader { + title: i18n("General") + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormCheckDelegate { + text: i18n("Close Astra when game is launched") + checked: LauncherCore.closeWhenLaunched + onCheckedChanged: LauncherCore.closeWhenLaunched = checked + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormCheckDelegate { + text: i18n("Show news banners") + checked: LauncherCore.showNewsBanners + onCheckedChanged: LauncherCore.showNewsBanners = checked + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormCheckDelegate { + text: i18n("Show news list") + checked: LauncherCore.showNewsList + onCheckedChanged: LauncherCore.showNewsList = checked + } + } +} \ No newline at end of file diff --git a/launcher/ui/Settings/ProfileSettings.qml b/launcher/ui/Settings/ProfileSettings.qml new file mode 100644 index 0000000..cef92dd --- /dev/null +++ b/launcher/ui/Settings/ProfileSettings.qml @@ -0,0 +1,281 @@ +// SPDX-FileCopyrightText: 2023 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Window 2.15 +import org.kde.kirigami 2.20 as Kirigami +import QtQuick.Controls 2.15 as Controls +import QtQuick.Layouts 1.15 +import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm +import com.redstrate.astra 1.0 + +import "../Components" + +Kirigami.ScrollablePage { + id: page + + property var profile + + title: i18n("Profile Settings") + + ColumnLayout { + width: parent.width + + MobileForm.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormCardHeader { + title: i18n("General") + } + + MobileForm.FormTextFieldDelegate { + label: i18n("Name") + text: page.profile.name + onTextChanged: page.profile.name = text + } + + MobileForm.FormDelegateSeparator { + } + + FormFolderDelegate { + text: i18n("Game Path") + folder: page.profile.gamePath + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormComboBoxDelegate { + text: i18n("DirectX Version") + model: ["DirectX 11", "DirectX 9"] + currentIndex: page.profile.directx9Enabled ? 1 : 0 + onCurrentIndexChanged: page.profile.directx9Enabled = (currentIndex === 1) + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormCheckDelegate { + text: i18n("Encrypt Game Arguments") + checked: page.profile.argumentsEncrypted + onCheckedChanged: page.profile.argumentsEncrypted = checked + enabled: false + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormCheckDelegate { + text: i18n("Enable Watchdog") + description: i18n("Gives real-time queue updates. X11 only.") + checked: page.profile.watchdogEnabled + onCheckedChanged: page.profile.watchdogEnabled = checked + enabled: false + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormTextDelegate { + description: page.profile.expansionVersionText + } + } + } + + MobileForm.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormCardHeader { + title: i18n("Wine") + } + + MobileForm.FormComboBoxDelegate { + text: i18n("Wine Type") + model: ["System", "Custom"] + currentIndex: page.profile.wineType + onCurrentIndexChanged: page.profile.wineType = currentIndex + enabled: !LauncherCore.isSteam + } + + MobileForm.FormDelegateSeparator { + } + + FormFileDelegate { + text: i18n("Wine Path") + file: page.profile.winePath + enabled: !LauncherCore.isSteam + } + + MobileForm.FormDelegateSeparator { + } + + FormFolderDelegate { + text: i18n("Wine Prefix Path") + folder: page.profile.winePrefixPath + enabled: !LauncherCore.isSteam + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormTextDelegate { + description: page.profile.wineVersionText + } + } + } + + MobileForm.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormCardHeader { + title: i18n("Tools") + } + + MobileForm.FormCheckDelegate { + text: i18n("Enable ESync") + description: i18n("Could improve game performance, but requires a patched Wine and kernel.") + checked: page.profile.esyncEnabled + onCheckedChanged: page.profile.esyncEnabled = checked + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormCheckDelegate { + text: i18n("Enable Gamescope") + description: i18n("A micro-compositor that uses Wayland to create a nested session.\nIf you use fullscreen mode, it may improve input handling.") + checked: page.profile.gamescopeEnabled + onCheckedChanged: page.profile.gamescopeEnabled = checked + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormButtonDelegate { + text: i18n("Configure Gamescope...") + icon.name: "configure" + enabled: page.profile.gamescopeEnabled + Kirigami.PromptDialog { + id: gamescopeSettingsDialog + title: i18n("Configure Gamescope") + + Kirigami.FormLayout { + Controls.CheckBox { + Kirigami.FormData.label: "Fullscreen:" + checked: page.profile.gamescopeFullscreen + onCheckedChanged: page.profile.gamescopeFullscreen = checked + } + Controls.CheckBox { + Kirigami.FormData.label: "Borderless:" + checked: page.profile.gamescopeBorderless + onCheckedChanged: page.profile.gamescopeBorderless = checked + } + Controls.SpinBox { + Kirigami.FormData.label: "Width:" + to: 4096 + value: page.profile.gamescopeWidth + onValueModified: page.profile.gamescopeWidth = value + } + Controls.SpinBox { + Kirigami.FormData.label: "Height:" + to: 4096 + value: page.profile.gamescopeHeight + onValueModified: page.profile.gamescopeHeight = value + } + Controls.SpinBox { + Kirigami.FormData.label: "Refresh Rate:" + to: 512 + value: page.profile.gamescopeRefreshRate + onValueModified: page.profile.gamescopeRefreshRate = value + } + } + } + + onClicked: gamescopeSettingsDialog.open() + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormCheckDelegate { + text: i18n("Enable Gamemode") + description: i18n("A special game performance tool, that tunes your CPU scheduler among other things.") + checked: page.profile.gamemodeEnabled + onCheckedChanged: page.profile.gamemodeEnabled = checked + } + } + } + + MobileForm.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormCardHeader { + title: i18n("Dalamud") + } + + MobileForm.FormCheckDelegate { + text: i18n("Enable Dalamud Plugins") + checked: page.profile.dalamudEnabled + onCheckedChanged: page.profile.dalamudEnabled = checked + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormComboBoxDelegate { + text: i18n("Update Channel") + model: ["Stable", "Staging", ".NET 5"] + currentIndex: page.profile.dalamudChannel + onCurrentIndexChanged: page.profile.dalamudChannel = currentIndex + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormCheckDelegate { + text: i18n("Opt Out of Automatic Marketboard Collection") + checked: page.profile.dalamudOptOut + onCheckedChanged: page.profile.dalamudOptOut = checked + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormTextDelegate { + description: page.profile.dalamudVersionText + } + } + } + + MobileForm.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormButtonDelegate { + text: i18n("Delete Profile") + description: !enabled ? i18n("Cannot delete the only profile") : "" + icon.name: "delete" + enabled: LauncherCore.profileManager.canDelete(page.profile) + onClicked: { + LauncherCore.profileManager.deleteProfile(page.profile) + applicationWindow().pageStack.layers.pop() + } + } + } + } + } +} \ No newline at end of file diff --git a/launcher/ui/Settings/SettingsPage.qml b/launcher/ui/Settings/SettingsPage.qml new file mode 100644 index 0000000..2deb22f --- /dev/null +++ b/launcher/ui/Settings/SettingsPage.qml @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: 2023 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Window 2.15 +import org.kde.kirigami 2.20 as Kirigami +import QtQuick.Controls 2.15 as Controls +import QtQuick.Layouts 1.15 +import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm +import com.redstrate.astra 1.0 + +Kirigami.ScrollablePage { + id: page + + title: i18n("Settings") + + ColumnLayout { + width: parent.width + + GeneralSettings { + } + + MobileForm.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormCardHeader { + title: i18n("Profiles") + } + + Repeater { + model: LauncherCore.profileManager + + MobileForm.FormButtonDelegate { + required property var profile + + text: profile.name + onClicked: applicationWindow().pageStack.layers.push('qrc:/ui/Settings/ProfileSettings.qml', { + profile: profile + }) + } + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormButtonDelegate { + text: i18n("Add Profile") + icon.name: "list-add" + onClicked: { + applicationWindow().currentSetupProfile = LauncherCore.profileManager.addProfile() + applicationWindow().checkSetup() + } + } + } + } + + MobileForm.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormCardHeader { + title: i18n("Accounts") + } + + Repeater { + model: LauncherCore.accountManager + + MobileForm.FormButtonDelegate { + required property var account + + text: account.name + + leading: Kirigami.Avatar + { + source: account.avatarUrl + } + + leadingPadding: Kirigami.Units.largeSpacing * 2 + + onClicked: applicationWindow().pageStack.layers.push('qrc:/ui/Settings/AccountSettings.qml', { + account: account + }) + } + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormButtonDelegate { + text: i18n("Add Square Enix Account") + icon.name: "list-add-symbolic" + onClicked: pageStack.layers.push('qrc:/ui/Setup/AddSquareEnix.qml') + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormButtonDelegate { + text: i18n("Add Sapphire Account") + icon.name: "list-add-symbolic" + onClicked: pageStack.layers.push('qrc:/ui/Setup/AddSapphire.qml') + } + } + } + + MobileForm.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 0 + Component { + id: aboutPage + MobileForm.AboutPage { + aboutData: About + } + } + + MobileForm.FormButtonDelegate { + text: i18n("About Astra") + icon.name: "help-about-symbolic" + onClicked: applicationWindow().pageStack.layers.push(aboutPage) + } + } + } + } +} \ No newline at end of file diff --git a/launcher/ui/Setup/AccountSetup.qml b/launcher/ui/Setup/AccountSetup.qml new file mode 100644 index 0000000..03a91fb --- /dev/null +++ b/launcher/ui/Setup/AccountSetup.qml @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2023 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Window 2.15 +import org.kde.kirigami 2.20 as Kirigami +import QtQuick.Controls 2.15 as Controls +import QtQuick.Layouts 1.15 +import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm +import com.redstrate.astra 1.0 + +Kirigami.Page { + id: page + + property var profile + + title: i18n("Account Setup") + + ColumnLayout { + width: parent.width + MobileForm.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormCardHeader { + title: i18n("Accounts") + } + + MobileForm.FormTextDelegate { + text: i18n("Select an account below to use.") + } + + Repeater { + model: LauncherCore.accountManager + + MobileForm.FormButtonDelegate { + required property var account + + text: account.name + + onClicked: { + page.profile.account = account + applicationWindow().checkSetup() + } + } + } + } + } + + MobileForm.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormButtonDelegate { + text: i18n("Add Square Enix Account") + icon.name: "list-add-symbolic" + onClicked: pageStack.layers.push('qrc:/ui/Setup/AddSquareEnix.qml', { + profile: page.profile + }) + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormButtonDelegate { + text: i18n("Add Sapphire Account") + icon.name: "list-add-symbolic" + onClicked: pageStack.layers.push('qrc:/ui/Setup/AddSapphire.qml', { + profile: page.profile + }) + } + } + } + } +} \ No newline at end of file diff --git a/launcher/ui/Setup/AddSapphire.qml b/launcher/ui/Setup/AddSapphire.qml new file mode 100644 index 0000000..47e0292 --- /dev/null +++ b/launcher/ui/Setup/AddSapphire.qml @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: 2023 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Window 2.15 +import org.kde.kirigami 2.20 as Kirigami +import QtQuick.Controls 2.15 as Controls +import QtQuick.Layouts 1.15 +import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm +import com.redstrate.astra 1.0 + +Kirigami.Page { + id: page + + property var profile + + title: i18n("Add Sapphire Account") + + ColumnLayout { + width: parent.width + MobileForm.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormTextDelegate { + description: i18n("Passwords will be entered on the login page. The username will be associated with this profile but can be changed later.") + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormTextFieldDelegate { + id: lobbyUrlField + label: i18n("Lobby URL") + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormTextFieldDelegate { + id: usernameField + label: i18n("Username") + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormButtonDelegate { + text: i18n("Add Account") + icon.name: "list-add-symbolic" + onClicked: { + let account = LauncherCore.accountManager.createSapphireAccount(lobbyUrlField.text, usernameField.text) + if (page.profile) { + page.profile.account = account + applicationWindow().checkSetup() + } else { + applicationWindow().pageStack.layers.pop() + } + } + } + } + } + } +} \ No newline at end of file diff --git a/launcher/ui/Setup/AddSquareEnix.qml b/launcher/ui/Setup/AddSquareEnix.qml new file mode 100644 index 0000000..0285db3 --- /dev/null +++ b/launcher/ui/Setup/AddSquareEnix.qml @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: 2023 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Window 2.15 +import org.kde.kirigami 2.20 as Kirigami +import QtQuick.Controls 2.15 as Controls +import QtQuick.Layouts 1.15 +import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm +import com.redstrate.astra 1.0 + +Kirigami.Page { + id: page + + property var profile + + title: i18n("Add Square Enix Account") + + ColumnLayout { + width: parent.width + MobileForm.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormTextDelegate { + description: i18n("Passwords will be entered on the login page. The username will be associated with this profile but can be changed later.") + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormTextFieldDelegate { + id: usernameField + label: i18n("Username") + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormComboBoxDelegate { + id: licenseField + text: i18n("License") + description: i18n("If the account holds multiple licenses, choose the preferred one.") + model: ["Windows", "Steam", "macOS"] + currentIndex: 0 + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormCheckDelegate { + id: freeTrialField + text: i18n("Free Trial") + description: i18n("Check if the account is currently on free trial.") + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormButtonDelegate { + text: i18n("Add Account") + icon.name: "list-add-symbolic" + onClicked: { + let account = LauncherCore.accountManager.createSquareEnixAccount(usernameField.text, licenseField.currentIndex, freeTrialField.checkState === Qt.Checked) + if (page.profile) { + page.profile.account = account + applicationWindow().checkSetup() + } else { + applicationWindow().pageStack.layers.pop() + } + } + } + } + } + } +} \ No newline at end of file diff --git a/launcher/ui/Setup/DownloadSetup.qml b/launcher/ui/Setup/DownloadSetup.qml new file mode 100644 index 0000000..4d2eae5 --- /dev/null +++ b/launcher/ui/Setup/DownloadSetup.qml @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2023 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Window 2.15 +import org.kde.kirigami 2.20 as Kirigami +import QtQuick.Controls 2.15 as Controls +import QtQuick.Layouts 1.15 +import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm +import com.redstrate.astra 1.0 + +Kirigami.Page { + id: page + + property var profile + + title: i18n("Download Game") + + ColumnLayout { + width: parent.width + MobileForm.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormCardHeader { + title: i18n("Download Game") + } + + MobileForm.FormTextDelegate { + text: i18n("Press the button below to download and setup the game.") + description: i18n("This is for the base files required for start-up, only when logged in will Astra begin downloading the full game.") + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormButtonDelegate { + text: i18n("Begin installation") + icon.name: "cloud-download" + onClicked: pageStack.layers.push('qrc:/ui/Setup/InstallProgress.qml', { + gameInstaller: LauncherCore.createInstaller(page.profile) + }) + } + } + } + } +} \ No newline at end of file diff --git a/launcher/ui/Setup/ExistingSetup.qml b/launcher/ui/Setup/ExistingSetup.qml new file mode 100644 index 0000000..a997fc4 --- /dev/null +++ b/launcher/ui/Setup/ExistingSetup.qml @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2023 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Window 2.15 +import org.kde.kirigami 2.20 as Kirigami +import QtQuick.Controls 2.15 as Controls +import QtQuick.Layouts 1.15 +import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm + +Kirigami.Page { + title: i18n("Find Game Installation") + + ColumnLayout { + width: parent.width + MobileForm.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormCardHeader { + title: i18n("Find an existing installation") + } + + MobileForm.FormTextDelegate { + text: i18n("Please select the path to your existing installation.") + } + } + } + } +} \ No newline at end of file diff --git a/launcher/ui/Setup/InstallProgress.qml b/launcher/ui/Setup/InstallProgress.qml new file mode 100644 index 0000000..7801e86 --- /dev/null +++ b/launcher/ui/Setup/InstallProgress.qml @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2023 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Window 2.15 +import org.kde.kirigami 2.20 as Kirigami +import QtQuick.Controls 2.15 as Controls +import QtQuick.Layouts 1.15 +import com.redstrate.astra 1.0 + +Kirigami.Page { + property var gameInstaller + + title: i18n("Game Installation") + + Kirigami.LoadingPlaceholder { + anchors.centerIn: parent + + text: i18n("Installing...") + } + + Component.onCompleted: gameInstaller.installGame() + + Connections { + target: gameInstaller + + function onInstallFinished() { + applicationWindow().checkSetup() + } + } +} \ No newline at end of file diff --git a/launcher/ui/Setup/SetupPage.qml b/launcher/ui/Setup/SetupPage.qml new file mode 100644 index 0000000..5a18c6e --- /dev/null +++ b/launcher/ui/Setup/SetupPage.qml @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2023 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Window 2.15 +import org.kde.kirigami 2.20 as Kirigami +import QtQuick.Controls 2.15 as Controls +import QtQuick.Layouts 1.15 +import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm + +Kirigami.Page { + id: page + + property var profile + + title: i18n("Game Setup") + + ColumnLayout { + width: parent.width + MobileForm.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormCardHeader { + title: i18n("Welcome to Astra") + } + + MobileForm.FormTextDelegate { + text: i18n("The game must be installed to continue. Please select a setup option below.") + description: i18n("A valid game account will be required at the end of installation.") + } + } + } + + MobileForm.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormButtonDelegate { + text: i18n("Find Existing Installation") + icon.name: "edit-find" + onClicked: pageStack.layers.push('qrc:/ui/Setup/ExistingSetup.qml', { + profile: page.profile + }) + } + + MobileForm.FormDelegateSeparator { + } + + MobileForm.FormButtonDelegate { + text: i18n("Download Game") + icon.name: "cloud-download" + onClicked: pageStack.layers.push('qrc:/ui/Setup/DownloadSetup.qml', { + profile: page.profile + }) + } + } + } + } +} \ No newline at end of file diff --git a/launcher/ui/main.qml b/launcher/ui/main.qml new file mode 100644 index 0000000..b1633a5 --- /dev/null +++ b/launcher/ui/main.qml @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2023 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Window 2.15 +import org.kde.kirigami 2.20 as Kirigami +import QtQuick.Controls 2.15 as Controls +import QtQuick.Layouts 1.15 +import com.redstrate.astra 1.0 + +import "Pages" + +Kirigami.ApplicationWindow { + id: appWindow + + width: 1280 + height: 720 + visible: true + title: LauncherCore.isSteam ? "Astra (Steam)" : "Astra" + + property var currentSetupProfile: LauncherCore.profileManager.getProfile(0) + + pageStack.initialPage: Kirigami.Page + { + Kirigami.LoadingPlaceholder { + anchors.centerIn: parent + } + } + + function checkSetup() { + if (!LauncherCore.loadingFinished) { + return + } + + pageStack.layers.clear() + + if (!currentSetupProfile.isGameInstalled) { + // User must set up the profile + pageStack.layers.replace('qrc:/ui/Setup/SetupPage.qml', { + profile: currentSetupProfile + }) + } else if (!currentSetupProfile.account) { + // User must select an account for the profile + pageStack.layers.replace('qrc:/ui/Setup/AccountSetup.qml', { + profile: currentSetupProfile + }) + } else { + pageStack.layers.replace('qrc:/ui/Pages/NewsPage.qml') + } + } + + Connections { + target: LauncherCore + + function onLoadingFinished() { + checkSetup() + } + } + + contextDrawer: LoginPage { + drawerOpen: true + modal: false + + edge: Qt.RightEdge + + topPadding: 0 + leftPadding: 0 + rightPadding: 0 + + width: 400 + } + + Component.onCompleted: checkSetup() +}