1
Fork 0
mirror of https://github.com/redstrate/Astra.git synced 2025-04-23 04:57:44 +00:00

Complete rewrite to Kirigami

Giant commit overhauling the interface to use KDE's Kirigami framework,
which is based on Qt Quick. The logic is all but rewritten, allowing
accounts to be separate from profiles.
This commit is contained in:
Joshua Goins 2023-07-30 08:49:34 -04:00
parent 558d02e344
commit 16420b7421
96 changed files with 4618 additions and 3817 deletions

View file

@ -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'

1
.gitignore vendored
View file

@ -5,3 +5,4 @@
.directory .directory
*.flatpak *.flatpak
export/ export/
.clang-format

View file

@ -1,30 +1,51 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(Astra) project(Astra VERSION 0.5.0 LANGUAGES CXX)
# build options used for distributors # build options used for distributors
option(BUILD_FLATPAK "Build for Flatpak." OFF) option(BUILD_FLATPAK "Build for Flatpak." OFF)
# options for features you may want or need # options for features you may want or need
option(ENABLE_WATCHDOG "Build support for Watchdog, requires an X11 system." OFF) option(ENABLE_WATCHDOG "Build support for Watchdog, requires X11." OFF)
option(ENABLE_STEAM "Build with Steam support, requires supplying the Steam SDK." 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_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_AUTOMOC ON)
set(CMAKE_AUTORCC 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) if (ENABLE_WATCHDOG)
find_package(PkgConfig REQUIRED)
pkg_search_module(TESSERACT REQUIRED tesseract) pkg_search_module(TESSERACT REQUIRED tesseract)
pkg_search_module(LEPTONICA REQUIRED lept) pkg_search_module(LEPTONICA REQUIRED lept)
endif () endif ()
if (ENABLE_GAMEMODE) if (ENABLE_GAMEMODE)
find_package(PkgConfig REQUIRED)
pkg_search_module(GAMEMODE REQUIRED gamemode) pkg_search_module(GAMEMODE REQUIRED gamemode)
endif () endif ()
@ -34,9 +55,9 @@ if (ENABLE_STEAM)
INTERFACE_INCLUDE_DIRECTORIES ${STEAMWORKS_INCLUDE_DIR} INTERFACE_INCLUDE_DIRECTORIES ${STEAMWORKS_INCLUDE_DIR}
IMPORTED_LOCATION ${STEAMWORKS_LIBRARIES}) IMPORTED_LOCATION ${STEAMWORKS_LIBRARIES})
if(BUILD_FLATPAK) if (BUILD_FLATPAK)
install(IMPORTED_RUNTIME_ARTIFACTS Steamworks) install(IMPORTED_RUNTIME_ARTIFACTS Steamworks)
endif() endif ()
endif () endif ()
find_package(Qt5Keychain REQUIRED) find_package(Qt5Keychain REQUIRED)
@ -44,3 +65,10 @@ find_package(QuaZip-Qt5 REQUIRED)
add_subdirectory(external) add_subdirectory(external)
add_subdirectory(launcher) 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)

View file

@ -1,7 +1,7 @@
# Astra # Astra
A custom FFXIV launcher that supports multiple accounts, [Dalamud](https://github.com/goatcorp/Dalamud) plugins and runs 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 ### Notice
@ -16,26 +16,22 @@ If you still have questions, please read the [FAQ](https://xiv.zone/astra/faq) f
## Features ## Features
* Traditional desktop interface which looks native to your system, utilizing Qt - a proven application framework. * Handles running Wine for you, creating a seamless and native-feeling launcher experience!
* 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.
* Can also easily enable several Linux-specific enhancements such as Fsync or configuring Gamescope. * Can also easily enable several Linux-specific enhancements such as Fsync or configuring Gamescope.
* Multiple account support! * Multiple account support!
* Most settings can be set per-profile. * Can associate a Lodestone character with an account to use as an avatar.
* Easily install and use Dalamud plugins, just like XIVQuickLauncher. * Easily install and use Dalamud plugins.
* Patches the game, just like the official launcher! * Game patching support.
* Securely login to the official Square Enix lobbies, as well as Sapphire servers. * 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. * 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. * 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 ## 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. * _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). * _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 `USE_OWN_LIBRARIES` CMake option.
[The wiki](https://man.sr.ht/~redstrate/astra/) has dedicated platform-specific pages for build instructions as well as [The wiki](https://man.sr.ht/~redstrate/astra/) has dedicated platform-specific pages for build instructions as well as
important information: [important usage information](https://man.sr.ht/~redstrate/astra/linux-usage.md).
* [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)
## Contributing and Support ## Contributing and Support

View file

@ -1,4 +1,5 @@
"manifest" "manifest"
{ {
"version" "2"
"commandline" "/astra --steam %verb%" "commandline" "/astra --steam %verb%"
} }

View file

@ -1,3 +1,5 @@
set(BUILD_SHARED_LIBS OFF)
add_subdirectory(libbaseencode) add_subdirectory(libbaseencode)
add_subdirectory(libcotp) add_subdirectory(libcotp)
@ -6,9 +8,8 @@ include(FetchContent)
FetchContent_Declare( FetchContent_Declare(
Corrosion Corrosion
GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git
GIT_TAG v0.3.5 GIT_TAG v0.4.2
) )
FetchContent_MakeAvailable(Corrosion) FetchContent_MakeAvailable(Corrosion)
FetchContent_Declare( FetchContent_Declare(
@ -16,7 +17,6 @@ FetchContent_Declare(
GIT_REPOSITORY https://git.sr.ht/~redstrate/libphysis GIT_REPOSITORY https://git.sr.ht/~redstrate/libphysis
GIT_TAG main GIT_TAG main
) )
FetchContent_MakeAvailable(libphysis) FetchContent_MakeAvailable(libphysis)
corrosion_import_crate(MANIFEST_PATH ${libphysis_SOURCE_DIR}/Cargo.toml corrosion_import_crate(MANIFEST_PATH ${libphysis_SOURCE_DIR}/Cargo.toml

View file

@ -1,29 +1,88 @@
add_subdirectory(core) add_executable(astra)
add_subdirectory(desktop) 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 src/account.cpp
main.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 resources.qrc)
astra_core kconfig_add_kcfg_files(astra GENERATE_MOC config.kcfgc accountconfig.kcfgc profileconfig.kcfgc)
astra_desktop) target_include_directories(astra PRIVATE include)
target_compile_features(astra PUBLIC cxx_std_17) target_link_libraries(astra PRIVATE
set_target_properties(astra PROPERTIES CXX_EXTENSIONS OFF) 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 if (ENABLE_WATCHDOG)
file(READ ${CMAKE_CURRENT_SOURCE_DIR}/../LICENSE LICENSE_TXT) target_sources(astra PRIVATE
STRING(REPLACE "\n" " \\n" LICENSE_TXT ${LICENSE_TXT}) include/gameparser.h
STRING(REPLACE "\"" "\"\"" LICENSE_TXT ${LICENSE_TXT}) include/watchdog.h
configure_file(${CMAKE_CURRENT_LIST_DIR}/../cmake/license.h.in src/gameparser.cpp
${CMAKE_BINARY_DIR}/license.h) 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) if (BUILD_FLATPAK)
target_compile_definitions(astra PRIVATE FLATPAK) target_compile_definitions(astra PRIVATE FLATPAK)
endif () endif ()
install(TARGETS astra if (ENABLE_STEAM)
DESTINATION "${INSTALL_BIN_PATH}") 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) if (WIN32)
get_target_property(QMAKE_EXE Qt5::qmake IMPORTED_LOCATION) get_target_property(QMAKE_EXE Qt5::qmake IMPORTED_LOCATION)

View file

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
<!--
SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
SPDX-License-Identifier: CC0-1.0
-->
<kcfgfile/>
<kcfgfile name="astrastaterc" stateConfig="true">
<parameter name="uuid"/>
</kcfgfile>
<group name="account-$(uuid)">
<entry key="Name" type="string">
</entry>
<entry key="LodestoneId" type="string">
</entry>
<entry key="IsSapphire" type="bool">
<default>false</default>
</entry>
<entry key="LobbyUrl" type="string">
</entry>
<entry key="RememberPassword" type="bool">
<default>false</default>
</entry>
<entry key="RememberOTP" type="bool">
<default>false</default>
</entry>
<entry key="UseOTP" type="bool">
<default>false</default>
</entry>
<entry name="License" type="Enum">
<choices>
<choice name="WindowsStandalone">
</choice>
<choice name="WindowsSteam">
</choice>
<choice name="macOS">
</choice>
</choices>
<default>WindowsStandalone</default>
</entry>
<entry key="IsFreeTrial" type="bool">
</entry>
</group>
</kcfg>

View file

@ -0,0 +1,9 @@
# SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
# SPDX-License-Identifier: LGPL-2.1-or-later
File=accountconfig.kcfg
ClassName=AccountConfig
Mutators=true
DefaultValueGetters=true
GenerateProperties=true
ParentInConstructor=true
Singleton=false

24
launcher/config.kcfg Normal file
View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
<!--
SPDX-FileCopyrightText: Joshua Goins <josh@redstrate.com>
SPDX-License-Identifier: CC0-1.0
-->
<kcfgfile name="astrarc" />
<group name="General">
<entry name="CloseWhenLaunched" type="bool">
<default>true</default>
</entry>
<entry name="ShowNewsBanners" type="bool">
<default>true</default>
</entry>
<entry name="ShowNewsList" type="bool">
<default>true</default>
</entry>
<entry name="AutoLogin" type="string">
</entry>
</group>
</kcfg>

9
launcher/config.kcfgc Normal file
View file

@ -0,0 +1,9 @@
# SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
# SPDX-License-Identifier: LGPL-2.1-or-later
File=config.kcfg
ClassName=Config
Mutators=true
DefaultValueGetters=true
GenerateProperties=true
ParentInConstructor=true
Singleton=true

View file

@ -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()

View file

@ -1,3 +0,0 @@
#pragma once
constexpr const char* version = "0.4.1";

View file

@ -1,42 +0,0 @@
#pragma once
#include <QString>
#include <physis.hpp>
// 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<uint8_t*>(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<uint8_t*>(toEncrypt.data()), toEncrypt.size(), &out_data, &out_size);
const QByteArray encryptedArg =
QByteArray::fromRawData(reinterpret_cast<const char*>(out_data), static_cast<int>(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));
}

View file

@ -1,10 +0,0 @@
#pragma once
#include <QString>
#include <functional>
class LauncherCore;
class ProfileSettings;
// TODO: convert to a nice signal/slots class like assetupdater
void installGame(LauncherCore& launcher, ProfileSettings& profile, const std::function<void()>& returnFunc);

View file

@ -1,31 +0,0 @@
#pragma once
#include <QDateTime>
#include <QUrl>
struct News {
QDateTime date;
QString id;
QString tag;
QString title;
QUrl url;
};
struct Banner {
QUrl link;
QUrl bannerImage;
};
struct Headline {
QList<Banner> banner;
QList<News> news;
QList<News> pinned;
QList<News> topics;
};
class LauncherCore;
void getHeadline(LauncherCore& core, const std::function<void(Headline)>& return_func);

View file

@ -1,266 +0,0 @@
#pragma once
#include <QFuture>
#include <QMainWindow>
#include <QMessageBox>
#include <QNetworkAccessManager>
#include <QProcess>
#include <QSettings>
#include <QUuid>
#include <QtQml>
#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<QString> 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<QString> 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*> profileSettings;
SteamAPI* steamApi = nullptr;
};

View file

@ -1,16 +0,0 @@
#pragma once
#include <QString>
#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;
};

View file

@ -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;
};

View file

@ -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;
};

View file

@ -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;
};

View file

@ -1,40 +0,0 @@
#include "encryptedarg.h"
#if defined(Q_OS_MAC)
#include <mach/mach_time.h>
#include <sys/sysctl.h>
#endif
#if defined(Q_OS_WIN)
#include <windows.h>
#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

View file

@ -1,84 +0,0 @@
#include "headline.h"
#include <QDateTime>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QUrlQuery>
#include "launchercore.h"
void getHeadline(LauncherCore& core, const std::function<void(Headline)>& 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);
});
}

View file

@ -1,725 +0,0 @@
#include <QComboBox>
#include <QCoreApplication>
#include <QDir>
#include <QFormLayout>
#include <QJsonDocument>
#include <QLineEdit>
#include <QMenuBar>
#include <QNetworkAccessManager>
#include <QProcess>
#include <QPushButton>
#include <QStandardPaths>
#include <algorithm>
#include <utility>
#include <qt5keychain/keychain.h>
#include <cotp.h>
#ifdef ENABLE_GAMEMODE
#include <gamemode_client.h>
#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<Argument> 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<QString> 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<QString>() &&
!settings.value("gamePath").toString().isEmpty()) {
profile->gamePath = settings.value("gamePath").toString();
} else {
profile->gamePath = getDefaultGamePath();
}
if (settings.contains("winePrefixPath") && settings.value("winePrefixPath").canConvert<QString>() &&
!settings.value("winePrefixPath").toString().isEmpty()) {
profile->winePrefixPath = settings.value("winePrefixPath").toString();
} else {
profile->winePrefixPath = getDefaultWinePrefixPath();
}
if (settings.contains("winePath") && settings.value("winePath").canConvert<QString>() &&
!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<QString> LauncherCore::profileList() const {
QList<QString> 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;
}

View file

@ -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)

View file

@ -1,8 +0,0 @@
#pragma once
#include "virtualdialog.h"
class AboutWindow : public VirtualDialog {
public:
explicit AboutWindow(DesktopInterface& interface, QWidget* widget = nullptr);
};

View file

@ -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();
};

View file

@ -1,17 +0,0 @@
#pragma once
#include <QLabel>
#include <QUrl>
class BannerWidget : public QLabel {
public:
BannerWidget();
void setUrl(QUrl url);
protected:
void mousePressEvent(QMouseEvent* event) override;
private:
QUrl url;
};

View file

@ -1,30 +0,0 @@
#pragma once
#include <QMdiArea>
#include <QMainWindow>
#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;
};

View file

@ -1,20 +0,0 @@
#pragma once
#include <QCheckBox>
#include <QComboBox>
#include <QDialog>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QPushButton>
#include "virtualdialog.h"
class LauncherCore;
class LauncherWindow;
struct ProfileSettings;
class GamescopeSettingsWindow : public VirtualDialog {
public:
GamescopeSettingsWindow(DesktopInterface& interface, ProfileSettings& settings, LauncherCore& core, QWidget* parent = nullptr);
};

View file

@ -1,67 +0,0 @@
#pragma once
#include <QCheckBox>
#include <QComboBox>
#include <QFormLayout>
#include <QGridLayout>
#include <QMainWindow>
#include <QPushButton>
#include <QScrollArea>
#include <QTreeWidget>
#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<QLabel*> 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;
};

View file

@ -1,89 +0,0 @@
#pragma once
#include <QCheckBox>
#include <QComboBox>
#include <QDialog>
#include <QFormLayout>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QPushButton>
#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;
};

View file

@ -1,28 +0,0 @@
#pragma once
#include <QMdiSubWindow>
#include <QWidget>
#include <QDialog>
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;
};

View file

@ -1,29 +0,0 @@
#pragma once
#include <QMdiSubWindow>
#include <QWidget>
#include <QMainWindow>
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;
};

View file

@ -1,78 +0,0 @@
#include "aboutwindow.h"
#include <QLabel>
#include <QPlainTextEdit>
#include <QTabWidget>
#include <QVBoxLayout>
#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("<h2>Astra</h2>\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("<a href='https://xiv.zone/astra'>https://xiv.zone/astra</a>");
websiteLabel->setOpenExternalLinks(true);
aboutLayout->addWidget(websiteLabel);
auto licenseLabel = new QLabel();
licenseLabel->setText("<a href='a'>License: GNU General Public License Version 3</a>");
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);
}

View file

@ -1,45 +0,0 @@
#include "autologinwindow.h"
#include <QDesktopServices>
#include <QFormLayout>
#include <QLabel>
#include <QPushButton>
#include <QSpinBox>
#include <QToolTip>
#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);
}

View file

@ -1,18 +0,0 @@
#include "bannerwidget.h"
#include <QDebug>
#include <QDesktopServices>
#include <utility>
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);
}

View file

@ -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);
}
}

View file

@ -1,67 +0,0 @@
#include "gamescopesettingswindow.h"
#include <QCheckBox>
#include <QDesktopServices>
#include <QFormLayout>
#include <QMessageBox>
#include <QSpinBox>
#include <QToolTip>
#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<int>::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<int>::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<int>::of(&QSpinBox::valueChanged), [&](int value) {
settings.gamescope.refreshRate = value;
core.saveSettings();
});
mainLayout->addRow("Refresh Rate", refreshRateBox);
}

View file

@ -1,556 +0,0 @@
#include "launcherwindow.h"
#include <QApplication>
#include <QDesktopServices>
#include <QDirIterator>
#include <QFormLayout>
#include <QHeaderView>
#include <QMenuBar>
#include <QNetworkReply>
#include <QScrollBar>
#include <QTimer>
#include <QTreeWidgetItem>
#include <utility>
#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<Argument> 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<void (QComboBox::*)(int)>(&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<QTreeWidgetItem*>({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
}

View file

@ -1,694 +0,0 @@
#include "settingswindow.h"
#include <QCheckBox>
#include <QDesktopServices>
#include <QFileDialog>
#include <QFormLayout>
#include <QGridLayout>
#include <QInputDialog>
#include <QLabel>
#include <QMessageBox>
#include <QPushButton>
#include <QToolTip>
#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<void (QComboBox::*)(int)>(&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<void (QComboBox::*)(int)>(&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<void (QComboBox::*)(int)>(&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<void (QComboBox::*)(int)>(&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<bool>(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<void (QComboBox::*)(int)>(&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);
}

View file

@ -1,75 +0,0 @@
#include "virtualdialog.h"
#include <QLayout>
#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;
}
}

View file

@ -1,77 +0,0 @@
#include "virtualwindow.h"
#include <QLayout>
#include <QMenuBar>
#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<QMenuBar*>(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;
}
}

View file

@ -0,0 +1,95 @@
#pragma once
#include <QObject>
#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;
};

View file

@ -0,0 +1,42 @@
#pragma once
#include <QAbstractListModel>
#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<int, QByteArray> 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<Account *> m_accounts;
LauncherCore &m_launcher;
};

View file

@ -2,21 +2,20 @@
#include <QJsonArray> #include <QJsonArray>
#include <QObject> #include <QObject>
#include <QProgressDialog>
#include <QTemporaryDir> #include <QTemporaryDir>
#include "launchercore.h" #include "launchercore.h"
class LauncherCore; class LauncherCore;
class QNetworkReply; class QNetworkReply;
struct ProfileSettings;
class AssetUpdater : public QObject { class AssetUpdater : public QObject
{
Q_OBJECT Q_OBJECT
public: public:
explicit AssetUpdater(LauncherCore& launcher); explicit AssetUpdater(Profile &profile, LauncherCore &launcher, QObject *parent = nullptr);
void update(const ProfileSettings& profile); void update();
void beginInstall(); void beginInstall();
void checkIfCheckingIsDone(); void checkIfCheckingIsDone();
@ -27,11 +26,9 @@ signals:
void finishedUpdating(); void finishedUpdating();
private: private:
LauncherCore& launcher; LauncherCore &launcher;
QProgressDialog* dialog; Profile::DalamudChannel chosenChannel;
DalamudChannel chosenChannel;
QString remoteDalamudVersion; QString remoteDalamudVersion;
QString remoteRuntimeVersion; QString remoteRuntimeVersion;
@ -49,4 +46,5 @@ private:
QJsonArray remoteDalamudAssetArray; QJsonArray remoteDalamudAssetArray;
QString dataDir; QString dataDir;
Profile &m_profile;
}; };

View file

@ -0,0 +1,5 @@
#pragma once
#include <QString>
QString encryptGameArg(const QString &arg);

View file

@ -0,0 +1,23 @@
#pragma once
#include <QObject>
#include <QString>
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;
};

View file

@ -4,14 +4,7 @@
#include <leptonica/allheaders.h> #include <leptonica/allheaders.h>
#include <tesseract/baseapi.h> #include <tesseract/baseapi.h>
enum class ScreenState { enum class ScreenState { Splash, LobbyError, WorldFull, ConnectingToDataCenter, EnteredTitleScreen, InLoginQueue };
Splash,
LobbyError,
WorldFull,
ConnectingToDataCenter,
EnteredTitleScreen,
InLoginQueue
};
struct GameParseResult { struct GameParseResult {
ScreenState state; ScreenState state;
@ -19,15 +12,18 @@ struct GameParseResult {
int playersInQueue = -1; 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; 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); return !(a == b);
} }
class GameParser { class GameParser
{
public: public:
GameParser(); GameParser();
~GameParser(); ~GameParser();
@ -35,5 +31,5 @@ public:
GameParseResult parseImage(QImage image); GameParseResult parseImage(QImage image);
private: private:
tesseract::TessBaseAPI* api; tesseract::TessBaseAPI *api;
}; };

View file

@ -0,0 +1,56 @@
#pragma once
#include <QDateTime>
#include <QObject>
#include <QUrl>
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<Banner> banners MEMBER banners CONSTANT)
Q_PROPERTY(QList<News> news MEMBER news CONSTANT)
Q_PROPERTY(QList<News> pinned MEMBER pinned CONSTANT)
Q_PROPERTY(QList<News> topics MEMBER topics CONSTANT)
public:
explicit Headline(QObject *parent = nullptr)
: QObject(parent)
{
}
QList<Banner> banners;
QList<News> news;
QList<News> pinned;
QList<News> topics;
};

182
launcher/include/launchercore.h Executable file
View file

@ -0,0 +1,182 @@
#pragma once
#include <QFuture>
#include <QNetworkAccessManager>
#include <QProcess>
#include <QtQml>
#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;
};

View file

@ -1,19 +1,19 @@
#pragma once #pragma once
#include <QNetworkAccessManager> #include <QNetworkAccessManager>
#include <QProgressDialog>
#include <QString> #include <QString>
#include <physis.hpp> #include <physis.hpp>
// General-purpose patcher routine. It opens a nice dialog box, handles downloading // General-purpose patcher routine. It opens a nice dialog box, handles downloading
// and processing patches. // and processing patches.
class Patcher : public QObject { class Patcher : public QObject
{
Q_OBJECT Q_OBJECT
public: public:
Patcher(QString baseDirectory, GameData* game_data); Patcher(QString baseDirectory, GameData *game_data, QObject *parent = nullptr);
Patcher(QString baseDirectory, BootData* game_data); Patcher(QString baseDirectory, BootData *game_data, QObject *parent = nullptr);
void processPatchList(QNetworkAccessManager& mgr, const QString& patchList); void processPatchList(QNetworkAccessManager &mgr, const QString &patchList);
signals: signals:
void done(); void done();
@ -21,7 +21,8 @@ signals:
private: private:
void checkIfDone(); void checkIfDone();
[[nodiscard]] bool isBoot() const { [[nodiscard]] bool isBoot() const
{
return boot_data != nullptr; return boot_data != nullptr;
} }
@ -29,15 +30,13 @@ private:
QString name, repository, version, path; QString name, repository, version, path;
}; };
void processPatch(const QueuedPatch& patch); void processPatch(const QueuedPatch &patch);
QVector<QueuedPatch> patchQueue; QVector<QueuedPatch> patchQueue;
QString baseDirectory; QString baseDirectory;
BootData* boot_data = nullptr; BootData *boot_data = nullptr;
GameData* game_data = nullptr; GameData *game_data = nullptr;
QProgressDialog* dialog = nullptr;
int remainingPatches = -1; int remainingPatches = -1;
}; };

181
launcher/include/profile.h Normal file
View file

@ -0,0 +1,181 @@
#pragma once
#include <QObject>
#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<QString> 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;
};

View file

@ -0,0 +1,45 @@
#pragma once
#include <QAbstractListModel>
#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<int, QByteArray> 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<Profile *> profiles() const;
Q_INVOKABLE bool canDelete(Profile *account) const;
private:
void insertProfile(Profile *profile);
QString getDefaultGamePath();
QString getDefaultWinePrefixPath();
QVector<Profile *> m_profiles;
LauncherCore &m_launcher;
};

View file

@ -0,0 +1,17 @@
#pragma once
#include <QString>
#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;
};

View file

@ -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;
};

View file

@ -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;
};

View file

@ -0,0 +1,18 @@
#pragma once
#include <QObject>
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;
};

View file

@ -5,21 +5,26 @@
#include "launchercore.h" #include "launchercore.h"
#if defined(Q_OS_LINUX) #if defined(Q_OS_LINUX)
#include "gameparser.h" #include "gameparser.h"
#endif #endif
#include <QSystemTrayIcon> #include <QSystemTrayIcon>
class Watchdog : public QObject { class Watchdog : public QObject
{
Q_OBJECT Q_OBJECT
public: 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: private:
LauncherCore& core; LauncherCore &core;
QSystemTrayIcon* icon = nullptr; QSystemTrayIcon *icon = nullptr;
int processWindowId = -1; int processWindowId = -1;

View file

@ -1,50 +0,0 @@
#include "launchercore.h"
#include <QApplication>
#include <QCommandLineParser>
#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<DesktopInterface>(c);
return QApplication::exec();
}

View file

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
<!--
SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
SPDX-License-Identifier: CC0-1.0
-->
<kcfgfile/>
<kcfgfile name="astrastaterc" stateConfig="true">
<parameter name="uuid"/>
</kcfgfile>
<group name="profile-$(uuid)">
<entry key="Name" type="string">
</entry>
<entry key="Account" type="string">
</entry>
<entry key="Language" type="int">
</entry>
<entry key="GamePath" type="Path">
</entry>
<entry key="WinePath" type="Path">
</entry>
<entry key="WinePrefixPath" type="Path">
</entry>
<entry key="EnableWatchdog" type="bool">
<default>false</default>
</entry>
<entry key="WineType" type="Enum">
<choices>
<choice name="System">
</choice>
<choice name="Custom">
</choice>
<choice name="BuiltIn">
</choice>
<choice name="XIVOnMac">
</choice>
</choices>
<default>System</default>
</entry>
<entry key="UseESync" type="bool">
<default>false</default>
</entry>
<entry key="UseGamescope" type="bool">
<default>false</default>
</entry>
<entry key="UseGamemode" type="bool">
<default>false</default>
</entry>
<entry key="UseDX9" type="bool">
<default>false</default>
</entry>
<entry key="GamescopeFullscreen" type="bool">
<default>false</default>
</entry>
<entry key="GamescopeBorderless" type="bool">
<default>false</default>
</entry>
<entry key="GamescopeWidth" type="int">
<default>1280</default>
</entry>
<entry key="GamescopeHeight" type="int">
<default>720</default>
</entry>
<entry key="GamescopeRefreshRate" type="int">
<default>60</default>
</entry>
<entry key="DalamudEnabled" type="bool">
<default>false</default>
</entry>
<entry key="DalamudOptOut" type="bool">
<default>false</default>
</entry>
<entry key="DalamudChannel" type="Enum">
<choices>
<choice name="Stable">
</choice>
<choice name="Staging">
</choice>
<choice name="Net5">
</choice>
</choices>
<default>Stable</default>
</entry>
<entry key="EncryptArguments" type="bool">
<default>true</default>
</entry>
</group>
</kcfg>

View file

@ -0,0 +1,9 @@
# SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
# SPDX-License-Identifier: LGPL-2.1-or-later
File=profileconfig.kcfg
ClassName=ProfileConfig
Mutators=true
DefaultValueGetters=true
GenerateProperties=true
ParentInConstructor=true
Singleton=false

21
launcher/resources.qrc Normal file
View file

@ -0,0 +1,21 @@
<RCC>
<qresource prefix="/">
<file>ui/Components/FormFileDelegate.qml</file>
<file>ui/Components/FormFolderDelegate.qml</file>
<file>ui/Pages/LoginPage.qml</file>
<file>ui/Pages/NewsPage.qml</file>
<file>ui/Pages/StatusPage.qml</file>
<file>ui/Settings/AccountSettings.qml</file>
<file>ui/Settings/GeneralSettings.qml</file>
<file>ui/Settings/ProfileSettings.qml</file>
<file>ui/Settings/SettingsPage.qml</file>
<file>ui/Setup/AccountSetup.qml</file>
<file>ui/Setup/AddSapphire.qml</file>
<file>ui/Setup/AddSquareEnix.qml</file>
<file>ui/Setup/DownloadSetup.qml</file>
<file>ui/Setup/ExistingSetup.qml</file>
<file>ui/Setup/InstallProgress.qml</file>
<file>ui/Setup/SetupPage.qml</file>
<file>ui/main.qml</file>
</qresource>
</RCC>

222
launcher/src/account.cpp Normal file
View file

@ -0,0 +1,222 @@
#include "account.h"
#include <QEventLoop>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <qt5keychain/keychain.h>
#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<GameLicense>(m_config.license());
}
void Account::setLicense(const GameLicense license)
{
if (static_cast<GameLicense>(m_config.license()) != license) {
m_config.setLicense(static_cast<int>(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;
}

View file

@ -0,0 +1,106 @@
#include "accountmanager.h"
#include <KSharedConfig>
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<int, QByteArray> 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<Account::GameLicense>(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();
}

View file

@ -3,14 +3,11 @@
#include <QFile> #include <QFile>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply> #include <QNetworkReply>
#include <QStandardPaths> #include <QStandardPaths>
#include <JlCompress.h> #include <JlCompress.h>
#include "launchercore.h"
const QString baseGoatDomain = "https://goatcorp.github.io"; const QString baseGoatDomain = "https://goatcorp.github.io";
const QString baseDalamudDistribution = baseGoatDomain + "/dalamud-distrib/"; const QString baseDalamudDistribution = baseGoatDomain + "/dalamud-distrib/";
@ -20,17 +17,18 @@ const QString dalamudVersionManifestURL = baseDalamudDistribution + "%1version";
const QString baseDalamudAssetDistribution = baseGoatDomain + "/DalamudAssets"; const QString baseDalamudAssetDistribution = baseGoatDomain + "/DalamudAssets";
const QString dalamudAssetManifestURL = baseDalamudAssetDistribution + "/asset.json"; const QString dalamudAssetManifestURL = baseDalamudAssetDistribution + "/asset.json";
const QString dotnetRuntimePackageURL = const QString dotnetRuntimePackageURL = "https://dotnetcli.azureedge.net/dotnet/Runtime/%1/dotnet-runtime-%1-win-x64.zip";
"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 dotnetDesktopPackageURL =
"https://dotnetcli.azureedge.net/dotnet/WindowsDesktop/%1/windowsdesktop-runtime-%1-win-x64.zip";
QMap<DalamudChannel, QString> channelToDistribPrefix = { QMap<Profile::DalamudChannel, QString> channelToDistribPrefix = {{Profile::DalamudChannel::Stable, "/"},
{DalamudChannel::Stable, "/"}, {Profile::DalamudChannel::Staging, "stg/"},
{DalamudChannel::Staging, "stg/"}, {Profile::DalamudChannel::Net5, "net5/"}};
{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); launcher.mgr->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
dataDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); dataDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
@ -39,19 +37,20 @@ AssetUpdater::AssetUpdater(LauncherCore& launcher) : launcher(launcher), QObject
QDir().mkdir(dataDir); QDir().mkdir(dataDir);
} }
void AssetUpdater::update(const ProfileSettings& profile) { void AssetUpdater::update()
{
// non-dalamud users can bypass this process since it's not needed // non-dalamud users can bypass this process since it's not needed
if (!profile.dalamud.enabled) { if (!m_profile.dalamudEnabled()) {
finishedUpdating(); finishedUpdating();
return; 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 // first, we want to collect all of the remote versions
qInfo() << "Starting update sequence..."; qInfo() << "Starting update sequence...";
dialog->setLabelText("Checking for updates..."); // dialog->setLabelText("Checking for updates...");
// dalamud assets // dalamud assets
{ {
@ -65,14 +64,14 @@ void AssetUpdater::update(const ProfileSettings& profile) {
QNetworkRequest request(dalamudAssetManifestURL); QNetworkRequest request(dalamudAssetManifestURL);
auto reply = launcher.mgr->get(request); auto reply = launcher.mgr->get(request);
connect(reply, &QNetworkReply::finished, [reply, this, &profile] { connect(reply, &QNetworkReply::finished, [reply, this] {
dialog->setLabelText("Checking for Dalamud asset updates..."); // dialog->setLabelText("Checking for Dalamud asset updates...");
// TODO: handle asset failure // TODO: handle asset failure
QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
qInfo() << "Dalamud asset remote version" << doc.object()["Version"].toInt(); 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(); remoteDalamudAssetVersion = doc.object()["Version"].toInt();
@ -85,16 +84,16 @@ void AssetUpdater::update(const ProfileSettings& profile) {
// dalamud injector / net runtime // dalamud injector / net runtime
// they're all updated in unison, so there's no reason to have multiple checks // 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(); remoteDalamudVersion.clear();
remoteRuntimeVersion.clear(); remoteRuntimeVersion.clear();
auto reply = launcher.mgr->get(request); auto reply = launcher.mgr->get(request);
connect(reply, &QNetworkReply::finished, [this, &profile, reply] { connect(reply, &QNetworkReply::finished, [this, reply] {
dialog->setLabelText("Checking for Dalamud updates..."); // dialog->setLabelText("Checking for Dalamud updates...");
QByteArray str = reply->readAll(); QByteArray str = reply->readAll();
// for some god forsaken reason, the version string comes back as raw // 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) { if (needsDalamudInstall) {
bool success = !JlCompress::extractDir(tempDir.path() + "/latest.zip", dataDir + "/Dalamud").empty(); bool success = !JlCompress::extractDir(tempDir.path() + "/latest.zip", dataDir + "/Dalamud").empty();
@ -132,8 +132,7 @@ void AssetUpdater::beginInstall() {
} }
if (needsRuntimeInstall) { if (needsRuntimeInstall) {
bool success = bool success = !JlCompress::extractDir(tempDir.path() + "/dotnet-core.zip", dataDir + "/DalamudRuntime").empty();
!JlCompress::extractDir(tempDir.path() + "/dotnet-core.zip", dataDir + "/DalamudRuntime").empty();
success |= !JlCompress::extractDir(tempDir.path() + "/dotnet-desktop.zip", dataDir + "/DalamudRuntime").empty(); success |= !JlCompress::extractDir(tempDir.path() + "/dotnet-desktop.zip", dataDir + "/DalamudRuntime").empty();
@ -152,14 +151,15 @@ void AssetUpdater::beginInstall() {
checkIfFinished(); checkIfFinished();
} }
void AssetUpdater::checkIfDalamudAssetsDone() { void AssetUpdater::checkIfDalamudAssetsDone()
if (dialog->wasCanceled()) {
return; // if (dialog->wasCanceled())
// return;
if (dalamudAssetNeededFilenames.empty()) { if (dalamudAssetNeededFilenames.empty()) {
qInfo() << "Finished downloading Dalamud assets."; qInfo() << "Finished downloading Dalamud assets.";
launcher.dalamudAssetVersion = remoteDalamudAssetVersion; m_profile.dalamudAssetVersion = remoteDalamudAssetVersion;
QFile file(dataDir + "/DalamudAssets/" + "asset.ver"); QFile file(dataDir + "/DalamudAssets/" + "asset.ver");
file.open(QIODevice::WriteOnly | QIODevice::Text); file.open(QIODevice::WriteOnly | QIODevice::Text);
@ -170,26 +170,27 @@ void AssetUpdater::checkIfDalamudAssetsDone() {
} }
} }
void AssetUpdater::checkIfFinished() { void AssetUpdater::checkIfFinished()
if (dialog->wasCanceled()) {
return; // if (dialog->wasCanceled())
// return;
if (doneDownloadingDalamud && doneDownloadingRuntimeCore && if (doneDownloadingDalamud && doneDownloadingRuntimeCore && doneDownloadingRuntimeDesktop && dalamudAssetNeededFilenames.empty()) {
doneDownloadingRuntimeDesktop && dalamudAssetNeededFilenames.empty()) {
if (needsRuntimeInstall || needsDalamudInstall) { if (needsRuntimeInstall || needsDalamudInstall) {
beginInstall(); beginInstall();
} else { } else {
dialog->setLabelText("Finished!"); // dialog->setLabelText("Finished!");
dialog->close(); // dialog->close();
finishedUpdating(); finishedUpdating();
} }
} }
} }
void AssetUpdater::checkIfCheckingIsDone() { void AssetUpdater::checkIfCheckingIsDone()
if (dialog->wasCanceled()) {
return; // if (dialog->wasCanceled())
// return;
if (remoteDalamudVersion.isEmpty() || remoteRuntimeVersion.isEmpty() || remoteDalamudAssetVersion == -1) { if (remoteDalamudVersion.isEmpty() || remoteRuntimeVersion.isEmpty() || remoteDalamudAssetVersion == -1) {
return; return;
@ -198,10 +199,10 @@ void AssetUpdater::checkIfCheckingIsDone() {
// now that we got all the information we need, let's check if anything is // now that we got all the information we need, let's check if anything is
// updateable // updateable
dialog->setLabelText("Starting update..."); // dialog->setLabelText("Starting update...");
// dalamud injector / net runtime // dalamud injector / net runtime
if (launcher.runtimeVersion != remoteRuntimeVersion) { if (m_profile.runtimeVersion != remoteRuntimeVersion) {
needsRuntimeInstall = true; needsRuntimeInstall = true;
// core // core
@ -212,7 +213,7 @@ void AssetUpdater::checkIfCheckingIsDone() {
connect(reply, &QNetworkReply::finished, [this, reply] { connect(reply, &QNetworkReply::finished, [this, reply] {
qInfo() << "Dotnet-core finished downloading!"; qInfo() << "Dotnet-core finished downloading!";
dialog->setLabelText("Updating Dotnet-core..."); // dialog->setLabelText("Updating Dotnet-core...");
QFile file(tempDir.path() + "/dotnet-core.zip"); QFile file(tempDir.path() + "/dotnet-core.zip");
file.open(QIODevice::WriteOnly); file.open(QIODevice::WriteOnly);
@ -233,7 +234,7 @@ void AssetUpdater::checkIfCheckingIsDone() {
connect(reply, &QNetworkReply::finished, [this, reply] { connect(reply, &QNetworkReply::finished, [this, reply] {
qInfo() << "Dotnet-desktop finished downloading!"; qInfo() << "Dotnet-desktop finished downloading!";
dialog->setLabelText("Updating Dotnet-desktop..."); // dialog->setLabelText("Updating Dotnet-desktop...");
QFile file(tempDir.path() + "/dotnet-desktop.zip"); QFile file(tempDir.path() + "/dotnet-desktop.zip");
file.open(QIODevice::WriteOnly); file.open(QIODevice::WriteOnly);
@ -253,7 +254,7 @@ void AssetUpdater::checkIfCheckingIsDone() {
checkIfFinished(); checkIfFinished();
} }
if (remoteDalamudVersion != launcher.dalamudVersion) { if (remoteDalamudVersion != m_profile.dalamudVersion) {
qInfo() << "Downloading Dalamud..."; qInfo() << "Downloading Dalamud...";
needsDalamudInstall = true; needsDalamudInstall = true;
@ -264,7 +265,7 @@ void AssetUpdater::checkIfCheckingIsDone() {
connect(reply, &QNetworkReply::finished, [this, reply] { connect(reply, &QNetworkReply::finished, [this, reply] {
qInfo() << "Dalamud finished downloading!"; qInfo() << "Dalamud finished downloading!";
dialog->setLabelText("Updating Dalamud..."); // dialog->setLabelText("Updating Dalamud...");
QFile file(tempDir.path() + "/latest.zip"); QFile file(tempDir.path() + "/latest.zip");
file.open(QIODevice::WriteOnly); file.open(QIODevice::WriteOnly);
@ -273,7 +274,7 @@ void AssetUpdater::checkIfCheckingIsDone() {
doneDownloadingDalamud = true; doneDownloadingDalamud = true;
launcher.dalamudVersion = remoteDalamudVersion; m_profile.dalamudVersion = remoteDalamudVersion;
checkIfFinished(); checkIfFinished();
}); });
@ -287,10 +288,10 @@ void AssetUpdater::checkIfCheckingIsDone() {
} }
// dalamud assets // dalamud assets
if (remoteDalamudAssetVersion != launcher.dalamudAssetVersion) { if (remoteDalamudAssetVersion != m_profile.dalamudAssetVersion) {
qInfo() << "Dalamud assets out of date."; qInfo() << "Dalamud assets out of date.";
dialog->setLabelText("Updating Dalamud assets..."); // dialog->setLabelText("Updating Dalamud assets...");
dalamudAssetNeededFilenames.clear(); dalamudAssetNeededFilenames.clear();
@ -309,7 +310,7 @@ void AssetUpdater::checkIfCheckingIsDone() {
const QList<QString> dirPath = fileName.left(fileName.lastIndexOf("/")).split('/'); const QList<QString> dirPath = fileName.left(fileName.lastIndexOf("/")).split('/');
QString build = dataDir + "/DalamudAssets/"; QString build = dataDir + "/DalamudAssets/";
for (const auto& dir : dirPath) { for (const auto &dir : dirPath) {
if (!QDir().exists(build + dir)) if (!QDir().exists(build + dir))
QDir().mkdir(build + dir); QDir().mkdir(build + dir);

View file

@ -0,0 +1,81 @@
#include "encryptedarg.h"
#include <physis.hpp>
#if defined(Q_OS_MAC)
#include <mach/mach_time.h>
#include <sys/sysctl.h>
#endif
#if defined(Q_OS_WIN)
#include <windows.h>
#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<uint8_t *>(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<uint8_t *>(toEncrypt.data()), toEncrypt.size(), &out_data, &out_size);
const QByteArray encryptedArg = QByteArray::fromRawData(reinterpret_cast<const char *>(out_data), static_cast<int>(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));
}

View file

@ -6,17 +6,25 @@
#include <physis.hpp> #include <physis.hpp>
#include "launchercore.h" #include "launchercore.h"
#include "profile.h"
void installGame(LauncherCore& launcher, ProfileSettings& profile, const std::function<void()>& returnFunc) { GameInstaller::GameInstaller(LauncherCore &launcher, Profile &profile, QObject *parent)
QString installDirectory = profile.gamePath; : QObject(parent)
, m_launcher(launcher)
, m_profile(profile)
{
}
void GameInstaller::installGame()
{
const QString installDirectory = m_profile.gamePath();
qDebug() << "Installing game to " << installDirectory << "!"; qDebug() << "Installing game to " << installDirectory << "!";
qDebug() << "Now downloading installer file..."; qDebug() << "Now downloading installer file...";
QNetworkRequest request(QUrl("https://gdl.square-enix.com/ffxiv/inst/ffxivsetup.exe")); QNetworkRequest request(QUrl("https://gdl.square-enix.com/ffxiv/inst/ffxivsetup.exe"));
auto reply = launcher.mgr->get(request); auto reply = m_launcher.mgr->get(request);
QObject::connect(reply, &QNetworkReply::finished, [reply, installDirectory, returnFunc] { QObject::connect(reply, &QNetworkReply::finished, [this, reply, installDirectory] {
QString dataDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); QString dataDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
QFile file(dataDir + "/ffxivsetup.exe"); QFile file(dataDir + "/ffxivsetup.exe");
@ -31,6 +39,6 @@ void installGame(LauncherCore& launcher, ProfileSettings& profile, const std::fu
qDebug() << "Done installing to " << installDirectory << "!"; qDebug() << "Done installing to " << installDirectory << "!";
returnFunc(); Q_EMIT installFinished();
}); });
} }

View file

@ -4,7 +4,8 @@
#include <QDebug> #include <QDebug>
#include <QRegularExpression> #include <QRegularExpression>
GameParser::GameParser() { GameParser::GameParser()
{
api = new tesseract::TessBaseAPI(); api = new tesseract::TessBaseAPI();
if (api->Init(nullptr, "eng")) { if (api->Init(nullptr, "eng")) {
@ -15,17 +16,19 @@ GameParser::GameParser() {
api->SetPageSegMode(tesseract::PageSegMode::PSM_SINGLE_BLOCK); api->SetPageSegMode(tesseract::PageSegMode::PSM_SINGLE_BLOCK);
} }
GameParser::~GameParser() { GameParser::~GameParser()
{
api->End(); api->End();
delete api; delete api;
} }
GameParseResult GameParser::parseImage(QImage img) { GameParseResult GameParser::parseImage(QImage img)
{
QBuffer buf; QBuffer buf;
img = img.convertToFormat(QImage::Format_Grayscale8); img = img.convertToFormat(QImage::Format_Grayscale8);
img.save(&buf, "PNG", 100); 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->SetImage(image);
api->SetSourceResolution(300); api->SetSourceResolution(300);

580
launcher/src/launchercore.cpp Executable file
View file

@ -0,0 +1,580 @@
#include "gameinstaller.h"
#include <QDir>
#include <QNetworkAccessManager>
#include <QProcess>
#include <QStandardPaths>
#include <algorithm>
#include <utility>
#ifdef ENABLE_GAMEMODE
#include <gamemode_client.h>
#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<Argument> 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<QString> 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<Argument> 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);
}

85
launcher/src/main.cpp Executable file
View file

@ -0,0 +1,85 @@
#include <KAboutData>
#include <KLocalizedContext>
#include <KLocalizedString>
#include <QApplication>
#include <QCommandLineParser>
#include <QQuickStyle>
#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<GameInstaller>("com.redstrate.astra", 1, 0, "GameInstaller", QStringLiteral("Use LauncherCore::createInstaller"));
qmlRegisterUncreatableType<AccountManager>("com.redstrate.astra", 1, 0, "AccountManager", QStringLiteral("Use LauncherCore::accountManager"));
qmlRegisterUncreatableType<ProfileManager>("com.redstrate.astra", 1, 0, "ProfileManager", QStringLiteral("Use LauncherCore::profileManager"));
qmlRegisterUncreatableType<Profile>("com.redstrate.astra", 1, 0, "Profile", QStringLiteral("Use from ProfileManager"));
qmlRegisterUncreatableType<Account>("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<Headline>("com.redstrate.astra", 1, 0, "Headline", QStringLiteral("Use from AccountManager"));
qRegisterMetaType<Banner>("Banner");
qRegisterMetaType<QList<Banner>>("QList<Banner>");
qRegisterMetaType<QList<News>>("QList<News>");
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();
}

View file

@ -1,4 +1,5 @@
#include "patcher.h" #include "patcher.h"
#include <QDir> #include <QDir>
#include <QFile> #include <QFile>
#include <QNetworkReply> #include <QNetworkReply>
@ -8,30 +9,39 @@
#include <physis.hpp> #include <physis.hpp>
#include <utility> #include <utility>
Patcher::Patcher(QString baseDirectory, BootData* boot_data) : boot_data(boot_data), baseDirectory(std::move(baseDirectory)) { Patcher::Patcher(QString baseDirectory, BootData *boot_data, QObject *parent)
dialog = new QProgressDialog(); : QObject(parent)
, baseDirectory(std::move(baseDirectory))
, boot_data(boot_data)
{
/*dialog = new QProgressDialog();
dialog->setLabelText("Checking the FINAL FANTASY XIV Updater/Launcher version."); 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)) { Patcher::Patcher(QString baseDirectory, GameData *game_data, QObject *parent)
dialog = new QProgressDialog(); : QObject(parent)
, baseDirectory(std::move(baseDirectory))
, game_data(game_data)
{
/*dialog = new QProgressDialog();
dialog->setLabelText("Checking the FINAL FANTASY XIV Game version."); 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()) { if (patchList.isEmpty()) {
dialog->hide(); // dialog->hide();
emit done(); emit done();
} else { } else {
if (isBoot()) { if (isBoot()) {
dialog->setLabelText("Updating the FINAL FANTASY XIV Updater/Launcher version."); // dialog->setLabelText("Updating the FINAL FANTASY XIV Updater/Launcher version.");
} else { } 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"); 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 QStringList patchParts = parts[i].split("\t");
const int length = patchParts[0].toInt(); const int length = patchParts[0].toInt();
Q_UNUSED(length)
QString name, url, version, repository; QString name, url, version, repository;
@ -60,7 +71,7 @@ void Patcher::processPatchList(QNetworkAccessManager& mgr, const QString& patchL
auto url_parts = url.split('/'); auto url_parts = url.split('/');
repository = url_parts[url_parts.size() - 3]; repository = url_parts[url_parts.size() - 3];
if (isBoot()) { /*if (isBoot()) {
dialog->setLabelText( dialog->setLabelText(
"Updating the FINAL FANTASY XIV Updater/Launcher version.\nDownloading ffxivboot - " + version); "Updating the FINAL FANTASY XIV Updater/Launcher version.\nDownloading ffxivboot - " + version);
} else { } else {
@ -69,10 +80,9 @@ void Patcher::processPatchList(QNetworkAccessManager& mgr, const QString& patchL
} }
dialog->setMinimum(0); dialog->setMinimum(0);
dialog->setMaximum(length); dialog->setMaximum(length);*/
const QString patchesDir = const QString patchesDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/patches/" + repository;
QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/patches/" + repository;
if (!QDir().exists(patchesDir)) if (!QDir().exists(patchesDir))
QDir().mkpath(patchesDir); QDir().mkpath(patchesDir);
@ -83,7 +93,9 @@ void Patcher::processPatchList(QNetworkAccessManager& mgr, const QString& patchL
QNetworkRequest patchRequest(url); QNetworkRequest patchRequest(url);
auto patchReply = mgr.get(patchRequest); auto patchReply = mgr.get(patchRequest);
connect(patchReply, &QNetworkReply::downloadProgress, [=](int recieved, int total) { connect(patchReply, &QNetworkReply::downloadProgress, [=](int recieved, int total) {
dialog->setValue(recieved); Q_UNUSED(recieved)
Q_UNUSED(total)
// dialog->setValue(recieved);
}); });
connect(patchReply, &QNetworkReply::finished, [=] { 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) { if (remainingPatches <= 0) {
for (const auto& patch : patchQueue) { for (const auto &patch : patchQueue) {
processPatch(patch); processPatch(patch);
} }
patchQueue.clear(); patchQueue.clear();
dialog->hide(); // dialog->hide();
emit done(); emit done();
} }
} }
void Patcher::processPatch(const QueuedPatch& patch) { void Patcher::processPatch(const QueuedPatch &patch)
{
if (isBoot()) { if (isBoot()) {
physis_bootdata_apply_patch(boot_data, patch.path.toStdString().c_str()); physis_bootdata_apply_patch(boot_data, patch.path.toStdString().c_str());
} else { } else {

495
launcher/src/profile.cpp Normal file
View file

@ -0,0 +1,495 @@
#include "profile.h"
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcess>
#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<WineType>(m_config.wineType());
}
void Profile::setWineType(const WineType type)
{
if (static_cast<WineType>(m_config.wineType()) != type) {
m_config.setWineType(static_cast<int>(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<DalamudChannel>(m_config.dalamudChannel());
}
void Profile::setDalamudChannel(const DalamudChannel value)
{
if (static_cast<DalamudChannel>(m_config.dalamudChannel()) != value) {
m_config.setDalamudChannel(static_cast<int>(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<unsigned int>(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;
}
}

View file

@ -0,0 +1,140 @@
#include "profilemanager.h"
#include <QDir>
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<int, QByteArray> 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<Profile *> ProfileManager::profiles() const
{
return m_profiles;
}
bool ProfileManager::canDelete(Profile *account) const
{
Q_UNUSED(account)
return m_profiles.size() != 1;
}

View file

@ -2,12 +2,16 @@
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QMessageBox>
#include <QNetworkReply> #include <QNetworkReply>
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}}; QJsonObject data{{"username", info.username}, {"pass", info.password}};
QUrl url(lobbyUrl + "/sapphire-api/lobby/login"); 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.frontierHost = document["frontierHost"].toString();
auth.region = 3; auth.region = 3;
window.launchGame(*info.settings, auth); window.launchGame(*info.profile, auth);
} else { } else {
auto messageBox = /*auto messageBox =
new QMessageBox(QMessageBox::Icon::Critical, "Failed to Login", "Invalid username/password."); 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}}; QJsonObject data{{"username", info.username}, {"pass", info.password}};
QUrl url(lobbyUrl + "/sapphire-api/lobby/createAccount"); 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.frontierHost = document["frontierHost"].toString();
auth.region = 3; auth.region = 3;
window.launchGame(*info.settings, auth); window.launchGame(*info.profile, auth);
}); });
} }

View file

@ -1,23 +1,30 @@
#include "squareboot.h" #include "squareboot.h"
#include <KLocalizedString>
#include <QFile> #include <QFile>
#include <QJsonDocument> #include <QJsonDocument>
#include <QMessageBox>
#include <QNetworkReply> #include <QNetworkReply>
#include <QPushButton>
#include <QStandardPaths> #include <QStandardPaths>
#include <QUrlQuery> #include <QUrlQuery>
#include <physis.hpp> #include <physis.hpp>
#include "account.h"
#include "squarelauncher.h" #include "squarelauncher.h"
SquareBoot::SquareBoot(LauncherCore& window, SquareLauncher& launcher) SquareBoot::SquareBoot(LauncherCore &window, SquareLauncher &launcher, QObject *parent)
: window(window), launcher(launcher), QObject(&window) {} : QObject(parent)
, window(window)
, launcher(launcher)
{
}
void SquareBoot::bootCheck(const LoginInformation& info) { void SquareBoot::bootCheck(const LoginInformation &info)
patcher = new Patcher(info.settings->gamePath + "/boot", info.settings->bootData); {
Q_EMIT window.stageChanged(i18n("Checking for launcher updates..."));
patcher = new Patcher(info.profile->gamePath() + "/boot", info.profile->bootData);
connect(patcher, &Patcher::done, [=, &info] { connect(patcher, &Patcher::done, [=, &info] {
window.readGameVersion(); info.profile->readGameVersion();
launcher.getStored(info); launcher.getStored(info);
}); });
@ -28,11 +35,11 @@ void SquareBoot::bootCheck(const LoginInformation& info) {
QUrl url; QUrl url;
url.setScheme("http"); url.setScheme("http");
url.setHost("patch-bootver.ffxiv.com"); 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); url.setQuery(query);
auto request = QNetworkRequest(url); 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"); request.setRawHeader("User-Agent", "FFXIV-MAC PATCH CLIENT");
} else { } else {
request.setRawHeader("User-Agent", "FFXIV PATCH CLIENT"); request.setRawHeader("User-Agent", "FFXIV PATCH CLIENT");
@ -41,21 +48,24 @@ void SquareBoot::bootCheck(const LoginInformation& info) {
request.setRawHeader("Host", "patch-bootver.ffxiv.com"); request.setRawHeader("Host", "patch-bootver.ffxiv.com");
auto reply = window.mgr->get(request); auto reply = window.mgr->get(request);
connect(reply, &QNetworkReply::finished, [=, &info] { connect(reply, &QNetworkReply::finished, [this, reply] {
const QString response = reply->readAll(); const QString response = reply->readAll();
patcher->processPatchList(*window.mgr, response); 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"); QUrl url("https://frontier.ffxiv.com/worldStatus/gate_status.json");
url.setQuery(QString::number(QDateTime::currentMSecsSinceEpoch())); url.setQuery(QString::number(QDateTime::currentMSecsSinceEpoch()));
QNetworkRequest request(url); QNetworkRequest request(url);
// TODO: really? // TODO: really?
window.buildRequest(*info->settings, request); window.buildRequest(*info->profile, request);
auto reply = window.mgr->get(request); auto reply = window.mgr->get(request);
connect(reply, &QNetworkReply::finished, [=] { 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. // 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 // 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. // "internet is unavailable" in some way.
if (reply->error() == QNetworkReply::HostNotFoundError || reply->error() == QNetworkReply::TimeoutError || if (reply->error() == QNetworkReply::HostNotFoundError || reply->error() == QNetworkReply::TimeoutError
reply->error() == QNetworkReply::UnknownServerError) || reply->error() == QNetworkReply::UnknownServerError)
checkGateStatus(info); checkGateStatus(info);
QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
@ -75,12 +85,7 @@ void SquareBoot::checkGateStatus(LoginInformation* info) {
if (isGateOpen) { if (isGateOpen) {
bootCheck(*info); bootCheck(*info);
} else { } else {
auto messageBox = new QMessageBox( Q_EMIT window.loginError(i18n("The login gate is closed, the game may be under maintenance."));
QMessageBox::Icon::Critical,
"Failed to Login",
"The login gate is closed, the game may be under maintenance.");
messageBox->show();
} }
}); });
} }

View file

@ -1,18 +1,23 @@
#include "squarelauncher.h" #include "squarelauncher.h"
#include <KLocalizedString>
#include <QDesktopServices> #include <QDesktopServices>
#include <QFile> #include <QFile>
#include <QMessageBox>
#include <QNetworkReply> #include <QNetworkReply>
#include <QPushButton>
#include <QRegularExpressionMatch> #include <QRegularExpressionMatch>
#include <QUrlQuery> #include <QUrlQuery>
#include "account.h"
#include "launchercore.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); auto f = QFile(file);
if (!f.open(QIODevice::ReadOnly)) if (!f.open(QIODevice::ReadOnly))
return ""; return "";
@ -23,18 +28,21 @@ QString getFileHash(const QString& file) {
return QString("%1/%2").arg(QString::number(f.size()), hash.result().toHex()); 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; QUrlQuery query;
// en is always used to the top url // en is always used to the top url
query.addQueryItem("lng", "en"); query.addQueryItem("lng", "en");
// for some reason, we always use region 3. the actual region is acquired later // for some reason, we always use region 3. the actual region is acquired later
query.addQueryItem("rgn", "3"); 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("cssmode", "1");
query.addQueryItem("isnew", "1"); query.addQueryItem("isnew", "1");
query.addQueryItem("launchver", "3"); query.addQueryItem("launchver", "3");
if (info.settings->license == GameLicense::WindowsSteam) { if (info.profile->account()->license() == Account::GameLicense::WindowsSteam) {
query.addQueryItem("issteam", "1"); query.addQueryItem("issteam", "1");
// TODO: get steam ticket information from steam api // TODO: get steam ticket information from steam api
@ -46,26 +54,22 @@ void SquareLauncher::getStored(const LoginInformation& info) {
url.setQuery(query); url.setQuery(query);
auto request = QNetworkRequest(url); 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] { connect(reply, &QNetworkReply::finished, [=, &info] {
auto str = QString(reply->readAll()); auto str = QString(reply->readAll());
// fetches Steam username // fetches Steam username
if (info.settings->license == GameLicense::WindowsSteam) { if (info.profile->account()->license() == Account::GameLicense::WindowsSteam) {
QRegularExpression re(R"lit(<input name=""sqexid"" type=""hidden"" value=""(?<sqexid>.*)""\/>)lit"); QRegularExpression re(R"lit(<input name=""sqexid"" type=""hidden"" value=""(?<sqexid>.*)""\/>)lit");
QRegularExpressionMatch match = re.match(str); QRegularExpressionMatch match = re.match(str);
if (match.hasMatch()) { if (match.hasMatch()) {
username = match.captured(1); username = match.captured(1);
} else { } else {
auto messageBox = new QMessageBox( Q_EMIT window.loginError(i18n("Could not get Steam username, have you attached your account?"));
QMessageBox::Icon::Critical,
"Failed to Login",
"Could not get Steam username, have you attached your account?");
messageBox->show();
} }
} else { } else {
username = info.username; username = info.username;
@ -77,16 +81,14 @@ void SquareLauncher::getStored(const LoginInformation& info) {
stored = match.captured(1); stored = match.captured(1);
login(info, url); login(info, url);
} else { } else {
auto messageBox = new QMessageBox( Q_EMIT window.loginError(
QMessageBox::Icon::Critical, i18n("Square Enix servers refused to confirm session information. The game may be under maintenance, try the official launcher."));
"Failed to Login",
"Failed to contact SE servers. They may be in maintenance.");
messageBox->show();
} }
}); });
} }
void SquareLauncher::login(const LoginInformation& info, const QUrl& referer) { void SquareLauncher::login(const LoginInformation &info, const QUrl &referer)
{
QUrlQuery postData; QUrlQuery postData;
postData.addQueryItem("_STORED_", stored); postData.addQueryItem("_STORED_", stored);
postData.addQueryItem("sqexid", info.username); postData.addQueryItem("sqexid", info.username);
@ -94,7 +96,7 @@ void SquareLauncher::login(const LoginInformation& info, const QUrl& referer) {
postData.addQueryItem("otppw", info.oneTimePassword); postData.addQueryItem("otppw", info.oneTimePassword);
QNetworkRequest request(QUrl("https://ffxiv-login.square-enix.com/oauth/ffxivarr/login/login.send")); 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.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
request.setRawHeader("Referer", referer.toEncoded()); request.setRawHeader("Referer", referer.toEncoded());
request.setRawHeader("Cache-Control", "no-cache"); 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"; const bool playable = parts[9] == "1";
if (!playable) { if (!playable) {
auto messageBox = new QMessageBox( Q_EMIT window.loginError(i18n("Your account is unplayable. Check that you have the correct license, and a valid subscription."));
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();
return; return;
} }
if (!terms) { if (!terms) {
auto messageBox = new QMessageBox( Q_EMIT window.loginError(i18n("Your account is unplayable. You need to accept the terms of service from the official launcher first."));
QMessageBox::Icon::Critical,
"Failed to Login",
"Your game is unplayable. You need to accept the terms of service from the official launcher.");
messageBox->show();
return; 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 // there's a stray quote at the end of the error string, so let's remove that
QString errorStr = match.captured(1).chopped(1); QString errorStr = match.captured(1).chopped(1);
auto messageBox = new QMessageBox(QMessageBox::Icon::Critical, "Failed to Login", errorStr); Q_EMIT window.loginError(errorStr);
messageBox->show();
} }
}); });
} }
void SquareLauncher::registerSession(const LoginInformation& info) { void SquareLauncher::registerSession(const LoginInformation &info)
{
QUrl url; QUrl url;
url.setScheme("https"); url.setScheme("https");
url.setHost("patch-gamever.ffxiv.com"); url.setHost("patch-gamever.ffxiv.com");
url.setPath(QString("/http/win32/ffxivneo_release_game/%1/%2") url.setPath(QString("/http/win32/ffxivneo_release_game/%1/%2").arg(info.profile->repositories.repositories[0].version, SID));
.arg(info.settings->repositories.repositories[0].version, SID));
auto request = QNetworkRequest(url); auto request = QNetworkRequest(url);
window.setSSL(request); window.setSSL(request);
@ -174,12 +155,11 @@ void SquareLauncher::registerSession(const LoginInformation& info) {
request.setRawHeader("User-Agent", "FFXIV PATCH CLIENT"); request.setRawHeader("User-Agent", "FFXIV PATCH CLIENT");
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); 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++) { for (int i = 1; i < auth.maxExpansion + 1; i++) {
if (i <= info.settings->repositories.repositories_count) { if (i <= static_cast<int>(info.profile->repositories.repositories_count)) {
report += report += QString("\nex%1\t%2").arg(QString::number(i), info.profile->repositories.repositories[i].version);
QString("\nex%1\t%2").arg(QString::number(i), info.settings->repositories.repositories[i].version);
} else { } else {
report += QString("\nex%1\t2012.01.01.0000.0000").arg(QString::number(i)); 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")) { if (reply->rawHeaderList().contains("X-Patch-Unique-Id")) {
QString body = reply->readAll(); 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] { connect(patcher, &Patcher::done, [=, &info] {
window.readGameVersion(); info.profile->readGameVersion();
auth.SID = reply->rawHeader("X-Patch-Unique-Id"); auth.SID = reply->rawHeader("X-Patch-Unique-Id");
window.launchGame(*info.settings, auth); window.launchGame(*info.profile, auth);
}); });
patcher->processPatchList(*window.mgr, body); patcher->processPatchList(*window.mgr, body);
} else { } else {
auto messageBox = new QMessageBox( Q_EMIT window.loginError(i18n("Fatal error, request was successful but X-Patch-Unique-Id was not recieved."));
QMessageBox::Icon::Critical,
"Failed to Login",
"Fatal error, request was successful but X-Patch-Unique-Id was not received");
messageBox->show();
} }
} else { } else {
if (reply->error() == QNetworkReply::SslHandshakeFailedError) { if (reply->error() == QNetworkReply::SslHandshakeFailedError) {
auto messageBox = new QMessageBox( Q_EMIT window.loginError(
QMessageBox::Icon::Critical, i18n("SSL handshake error detected. If you are using OpenSUSE or Fedora, try running `update-crypto-policies --set LEGACY`."));
"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();
} else if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 405) { } else if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 405) {
auto messageBox = new QMessageBox( Q_EMIT window.loginError(i18n("The game failed the anti-tamper check. Restore the game to the original state and try updating again."));
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();
} else { } else {
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); Q_EMIT window.loginError(i18n("Unknown error when registering the session."));
auto messageBox = new QMessageBox(
QMessageBox::Icon::Critical, "Failed to Login", &"Unknown error! Status code was "[statusCode]);
messageBox->show();
} }
} }
}); });
} }
QString SquareLauncher::getBootHash(const LoginInformation& info) { QString SquareLauncher::getBootHash(const LoginInformation &info)
const QList<QString> fileList = { {
"ffxivboot.exe", const QList<QString> fileList = {"ffxivboot.exe", "ffxivboot64.exe", "ffxivlauncher.exe", "ffxivlauncher64.exe", "ffxivupdater.exe", "ffxivupdater64.exe"};
"ffxivboot64.exe",
"ffxivlauncher.exe",
"ffxivlauncher64.exe",
"ffxivupdater.exe",
"ffxivupdater64.exe"};
QString result; QString result;
for (int i = 0; i < fileList.count(); i++) { 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) if (i != fileList.length() - 1)
result += ","; result += ",";

View file

@ -1,33 +1,39 @@
#include "steamapi.h" #include "steamapi.h"
#include "launchercore.h"
#ifdef ENABLE_STEAM #ifdef ENABLE_STEAM
#include <steam/steam_api.h> #include <steam/steam_api.h>
#endif #endif
SteamAPI::SteamAPI(LauncherCore& core) : core(core) { #include "launchercore.h"
SteamAPI::SteamAPI(LauncherCore &core, QObject *parent)
: QObject(parent)
, core(core)
{
#ifdef ENABLE_STEAM #ifdef ENABLE_STEAM
if(core.isSteam) { if (core.isSteam()) {
qputenv("SteamAppId", "39210"); qputenv("SteamAppId", "39210");
qputenv("SteamGameId", "39210"); qputenv("SteamGameId", "39210");
if(!SteamAPI_Init()) if (!SteamAPI_Init())
qDebug() << "Failed to initialize steam api!"; qDebug() << "Failed to initialize steam api!";
} }
#endif #endif
} }
void SteamAPI::setLauncherMode(bool isLauncher) { void SteamAPI::setLauncherMode(bool isLauncher)
{
#ifdef ENABLE_STEAM #ifdef ENABLE_STEAM
if(core.isSteam) { if (core.isSteam()) {
SteamUtils()->SetGameLauncherMode(isLauncher); SteamUtils()->SetGameLauncherMode(isLauncher);
} }
#endif #endif
} }
bool SteamAPI::isDeck() const { bool SteamAPI::isDeck() const
{
#ifdef ENABLE_STEAM #ifdef ENABLE_STEAM
if(core.isSteam) { if (core.isSteam()) {
return SteamUtils()->IsSteamRunningOnSteamDeck(); return SteamUtils()->IsSteamRunningOnSteamDeck();
} else { } else {
return false; return false;

View file

@ -13,33 +13,32 @@
// from https://github.com/adobe/webkit/blob/master/Source/WebCore/plugins/qt/QtX11ImageConversion.cpp // from https://github.com/adobe/webkit/blob/master/Source/WebCore/plugins/qt/QtX11ImageConversion.cpp
// code is licensed under GPLv2 // code is licensed under GPLv2
// Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies) // 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; QImage::Format format = QImage::Format_ARGB32_Premultiplied;
if (xi->depth == 24) if (xi->depth == 24)
format = QImage::Format_RGB32; format = QImage::Format_RGB32;
else if (xi->depth == 16) else if (xi->depth == 16)
format = QImage::Format_RGB16; format = QImage::Format_RGB16;
QImage image = QImage(reinterpret_cast<uchar*>(xi->data), xi->width, xi->height, xi->bytes_per_line, format).copy(); QImage image = QImage(reinterpret_cast<uchar *>(xi->data), xi->width, xi->height, xi->bytes_per_line, format).copy();
// we may have to swap the byte order // we may have to swap the byte order
if ((QSysInfo::ByteOrder == QSysInfo::LittleEndian && xi->byte_order == MSBFirst) || if ((QSysInfo::ByteOrder == QSysInfo::LittleEndian && xi->byte_order == MSBFirst)
(QSysInfo::ByteOrder == QSysInfo::BigEndian && xi->byte_order == LSBFirst)) { || (QSysInfo::ByteOrder == QSysInfo::BigEndian && xi->byte_order == LSBFirst)) {
for (int i = 0; i < image.height(); i++) { for (int i = 0; i < image.height(); i++) {
if (xi->depth == 16) { if (xi->depth == 16) {
ushort* p = reinterpret_cast<ushort*>(image.scanLine(i)); ushort *p = reinterpret_cast<ushort *>(image.scanLine(i));
ushort* end = p + image.width(); ushort *end = p + image.width();
while (p < end) { while (p < end) {
*p = ((*p << 8) & 0xff00) | ((*p >> 8) & 0x00ff); *p = ((*p << 8) & 0xff00) | ((*p >> 8) & 0x00ff);
p++; p++;
} }
} else { } else {
uint* p = reinterpret_cast<uint*>(image.scanLine(i)); uint *p = reinterpret_cast<uint *>(image.scanLine(i));
uint* end = p + image.width(); uint *end = p + image.width();
while (p < end) { while (p < end) {
*p = ((*p << 24) & 0xff000000) | ((*p << 8) & 0x00ff0000) | ((*p >> 8) & 0x0000ff00) | *p = ((*p << 24) & 0xff000000) | ((*p << 8) & 0x00ff0000) | ((*p >> 8) & 0x0000ff00) | ((*p >> 24) & 0x000000ff);
((*p >> 24) & 0x000000ff);
p++; p++;
} }
} }
@ -48,7 +47,7 @@ QImage qimageFromXImage(XImage* xi) {
// fix-up alpha channel // fix-up alpha channel
if (format == QImage::Format_RGB32) { if (format == QImage::Format_RGB32) {
QRgb* p = reinterpret_cast<QRgb*>(image.bits()); QRgb *p = reinterpret_cast<QRgb *>(image.bits());
for (int y = 0; y < xi->height; ++y) { for (int y = 0; y < xi->height; ++y) {
for (int x = 0; x < xi->width; ++x) for (int x = 0; x < xi->width; ++x)
p[x] |= 0xff000000; p[x] |= 0xff000000;
@ -59,7 +58,8 @@ QImage qimageFromXImage(XImage* xi) {
return image; return image;
} }
void Watchdog::launchGame(const ProfileSettings& settings, const LoginAuth& auth) { void Watchdog::launchGame(const ProfileSettings &settings, const LoginAuth &auth)
{
if (icon == nullptr) { if (icon == nullptr) {
icon = new QSystemTrayIcon(); icon = new QSystemTrayIcon();
} }
@ -91,20 +91,17 @@ void Watchdog::launchGame(const ProfileSettings& settings, const LoginAuth& auth
if (processWindowId == -1) { if (processWindowId == -1) {
auto xdoProcess = new QProcess(); auto xdoProcess = new QProcess();
connect( connect(xdoProcess, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), [=](int, QProcess::ExitStatus) {
xdoProcess, QString output = xdoProcess->readAllStandardOutput();
static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), qDebug() << "Found XIV Window: " << output.toInt();
[=](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 // TODO: don't use xdotool for this, find a better way to
xdoProcess->start("bash", {"-c", "xdotool search --name \"FINAL FANTASY XIV\""}); xdoProcess->start("bash", {"-c", "xdotool search --name \"FINAL FANTASY XIV\""});
} else { } else {
Display* display = XOpenDisplay(nullptr); Display *display = XOpenDisplay(nullptr);
XSynchronize(display, True); XSynchronize(display, True);
@ -119,7 +116,7 @@ void Watchdog::launchGame(const ProfileSettings& settings, const LoginAuth& auth
XCompositeRedirectWindow(display, processWindowId, CompositeRedirectAutomatic); XCompositeRedirectWindow(display, processWindowId, CompositeRedirectAutomatic);
XCompositeNameWindowPixmap(display, processWindowId); XCompositeNameWindowPixmap(display, processWindowId);
XRenderPictFormat* format = XRenderFindVisualFormat(display, attr.visual); XRenderPictFormat *format = XRenderFindVisualFormat(display, attr.visual);
XRenderPictureAttributes pa; XRenderPictureAttributes pa;
pa.subwindow_mode = IncludeInferiors; 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); Picture picture = XRenderCreatePicture(display, processWindowId, format, CPSubwindowMode, &pa);
XFlush(display); // TODO: does this actually make a difference? 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) { if (!image) {
qDebug() << "Unable to get image..."; qDebug() << "Unable to get image...";
} else { } else {
@ -138,26 +135,24 @@ void Watchdog::launchGame(const ProfileSettings& settings, const LoginAuth& auth
return; return;
switch (result.state) { switch (result.state) {
case ScreenState::InLoginQueue: { case ScreenState::InLoginQueue: {
icon->showMessage( icon->showMessage("Watchdog",
"Watchdog", QString("You are now at position %1 (moved %2 spots)")
QString("You are now at position %1 (moved %2 spots)") .arg(result.playersInQueue)
.arg(result.playersInQueue) .arg(lastResult.playersInQueue - result.playersInQueue));
.arg(lastResult.playersInQueue - result.playersInQueue));
icon->setToolTip(QString("Queue Status (%1)").arg(result.playersInQueue)); icon->setToolTip(QString("Queue Status (%1)").arg(result.playersInQueue));
} break; } break;
case ScreenState::LobbyError: { case ScreenState::LobbyError: {
// TODO: kill game? // TODO: kill game?
icon->showMessage("Watchdog", "You have been disconnected due to a lobby error."); icon->showMessage("Watchdog", "You have been disconnected due to a lobby error.");
} break; } break;
case ScreenState::ConnectingToDataCenter: { case ScreenState::ConnectingToDataCenter: {
icon->showMessage( icon->showMessage("Watchdog", "You are in the process of being connected to the data center.");
"Watchdog", "You are in the process of being connected to the data center."); } break;
} break; case ScreenState::WorldFull: {
case ScreenState::WorldFull: { icon->showMessage("Watchdog", "You have been disconnected due to a lobby error.");
icon->showMessage("Watchdog", "You have been disconnected due to a lobby error."); } break;
} break;
} }
lastResult = result; lastResult = result;

View file

@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
// 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
}
}

View file

@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
// 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
}
}

View file

@ -0,0 +1,216 @@
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
// 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)
}
}
}
}
}

View file

@ -0,0 +1,121 @@
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
// 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)
}
}
}
}
}
}

View file

@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
// 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()
}
}
}

View file

@ -0,0 +1,200 @@
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
// 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()
}
}
}
}
}
}

View file

@ -0,0 +1,49 @@
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
// 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
}
}
}

View file

@ -0,0 +1,281 @@
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
// 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()
}
}
}
}
}
}

View file

@ -0,0 +1,131 @@
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
// 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)
}
}
}
}
}

View file

@ -0,0 +1,79 @@
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
// 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
})
}
}
}
}
}

View file

@ -0,0 +1,66 @@
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
// 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()
}
}
}
}
}
}
}

View file

@ -0,0 +1,78 @@
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
// 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()
}
}
}
}
}
}
}

View file

@ -0,0 +1,49 @@
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
// 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)
})
}
}
}
}
}

View file

@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
// 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.")
}
}
}
}
}

View file

@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
// 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()
}
}
}

View file

@ -0,0 +1,64 @@
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
// 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
})
}
}
}
}
}

74
launcher/ui/main.qml Normal file
View file

@ -0,0 +1,74 @@
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
// 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()
}