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:
parent
558d02e344
commit
16420b7421
96 changed files with 4618 additions and 3817 deletions
|
@ -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
1
.gitignore
vendored
|
@ -5,3 +5,4 @@
|
||||||
.directory
|
.directory
|
||||||
*.flatpak
|
*.flatpak
|
||||||
export/
|
export/
|
||||||
|
.clang-format
|
|
@ -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)
|
26
README.md
26
README.md
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
"manifest"
|
"manifest"
|
||||||
{
|
{
|
||||||
|
"version" "2"
|
||||||
"commandline" "/astra --steam %verb%"
|
"commandline" "/astra --steam %verb%"
|
||||||
}
|
}
|
||||||
|
|
6
external/CMakeLists.txt
vendored
6
external/CMakeLists.txt
vendored
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
47
launcher/accountconfig.kcfg
Normal file
47
launcher/accountconfig.kcfg
Normal 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>
|
9
launcher/accountconfig.kcfgc
Normal file
9
launcher/accountconfig.kcfgc
Normal 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
24
launcher/config.kcfg
Normal 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
9
launcher/config.kcfgc
Normal 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
|
|
@ -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()
|
|
|
@ -1,3 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
constexpr const char* version = "0.4.1";
|
|
|
@ -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));
|
|
||||||
}
|
|
|
@ -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);
|
|
|
@ -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);
|
|
|
@ -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;
|
|
||||||
};
|
|
|
@ -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;
|
|
||||||
};
|
|
|
@ -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;
|
|
||||||
};
|
|
|
@ -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;
|
|
||||||
};
|
|
|
@ -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;
|
|
||||||
};
|
|
|
@ -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
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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)
|
|
|
@ -1,8 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "virtualdialog.h"
|
|
||||||
|
|
||||||
class AboutWindow : public VirtualDialog {
|
|
||||||
public:
|
|
||||||
explicit AboutWindow(DesktopInterface& interface, QWidget* widget = nullptr);
|
|
||||||
};
|
|
|
@ -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();
|
|
||||||
};
|
|
|
@ -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;
|
|
||||||
};
|
|
|
@ -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;
|
|
||||||
};
|
|
|
@ -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);
|
|
||||||
};
|
|
|
@ -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;
|
|
||||||
};
|
|
|
@ -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;
|
|
||||||
};
|
|
|
@ -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;
|
|
||||||
};
|
|
|
@ -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;
|
|
||||||
};
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
95
launcher/include/account.h
Normal file
95
launcher/include/account.h
Normal 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;
|
||||||
|
};
|
42
launcher/include/accountmanager.h
Normal file
42
launcher/include/accountmanager.h
Normal 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;
|
||||||
|
};
|
|
@ -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;
|
||||||
};
|
};
|
5
launcher/include/encryptedarg.h
Normal file
5
launcher/include/encryptedarg.h
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
QString encryptGameArg(const QString &arg);
|
23
launcher/include/gameinstaller.h
Normal file
23
launcher/include/gameinstaller.h
Normal 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;
|
||||||
|
};
|
|
@ -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;
|
||||||
};
|
};
|
56
launcher/include/headline.h
Normal file
56
launcher/include/headline.h
Normal 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
182
launcher/include/launchercore.h
Executable 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;
|
||||||
|
};
|
|
@ -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
181
launcher/include/profile.h
Normal 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;
|
||||||
|
};
|
45
launcher/include/profilemanager.h
Normal file
45
launcher/include/profilemanager.h
Normal 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;
|
||||||
|
};
|
17
launcher/include/sapphirelauncher.h
Normal file
17
launcher/include/sapphirelauncher.h
Normal 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;
|
||||||
|
};
|
24
launcher/include/squareboot.h
Normal file
24
launcher/include/squareboot.h
Normal 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;
|
||||||
|
};
|
27
launcher/include/squarelauncher.h
Normal file
27
launcher/include/squarelauncher.h
Normal 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;
|
||||||
|
};
|
18
launcher/include/steamapi.h
Normal file
18
launcher/include/steamapi.h
Normal 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;
|
||||||
|
};
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
91
launcher/profileconfig.kcfg
Normal file
91
launcher/profileconfig.kcfg
Normal 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>
|
9
launcher/profileconfig.kcfgc
Normal file
9
launcher/profileconfig.kcfgc
Normal 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
21
launcher/resources.qrc
Normal 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
222
launcher/src/account.cpp
Normal 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;
|
||||||
|
}
|
106
launcher/src/accountmanager.cpp
Normal file
106
launcher/src/accountmanager.cpp
Normal 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();
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
81
launcher/src/encryptedarg.cpp
Normal file
81
launcher/src/encryptedarg.cpp
Normal 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));
|
||||||
|
}
|
|
@ -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();
|
||||||
});
|
});
|
||||||
}
|
}
|
|
@ -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
580
launcher/src/launchercore.cpp
Executable 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
85
launcher/src/main.cpp
Executable 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();
|
||||||
|
}
|
|
@ -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
495
launcher/src/profile.cpp
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
140
launcher/src/profilemanager.cpp
Normal file
140
launcher/src/profilemanager.cpp
Normal 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;
|
||||||
|
}
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
|
@ -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();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
|
@ -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 += ",";
|
|
@ -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;
|
|
@ -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;
|
30
launcher/ui/Components/FormFileDelegate.qml
Normal file
30
launcher/ui/Components/FormFileDelegate.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
30
launcher/ui/Components/FormFolderDelegate.qml
Normal file
30
launcher/ui/Components/FormFolderDelegate.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
216
launcher/ui/Pages/LoginPage.qml
Normal file
216
launcher/ui/Pages/LoginPage.qml
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
121
launcher/ui/Pages/NewsPage.qml
Normal file
121
launcher/ui/Pages/NewsPage.qml
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
launcher/ui/Pages/StatusPage.qml
Normal file
45
launcher/ui/Pages/StatusPage.qml
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
200
launcher/ui/Settings/AccountSettings.qml
Normal file
200
launcher/ui/Settings/AccountSettings.qml
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
49
launcher/ui/Settings/GeneralSettings.qml
Normal file
49
launcher/ui/Settings/GeneralSettings.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
281
launcher/ui/Settings/ProfileSettings.qml
Normal file
281
launcher/ui/Settings/ProfileSettings.qml
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
131
launcher/ui/Settings/SettingsPage.qml
Normal file
131
launcher/ui/Settings/SettingsPage.qml
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
79
launcher/ui/Setup/AccountSetup.qml
Normal file
79
launcher/ui/Setup/AccountSetup.qml
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
66
launcher/ui/Setup/AddSapphire.qml
Normal file
66
launcher/ui/Setup/AddSapphire.qml
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
78
launcher/ui/Setup/AddSquareEnix.qml
Normal file
78
launcher/ui/Setup/AddSquareEnix.qml
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
49
launcher/ui/Setup/DownloadSetup.qml
Normal file
49
launcher/ui/Setup/DownloadSetup.qml
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
launcher/ui/Setup/ExistingSetup.qml
Normal file
32
launcher/ui/Setup/ExistingSetup.qml
Normal 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.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
launcher/ui/Setup/InstallProgress.qml
Normal file
31
launcher/ui/Setup/InstallProgress.qml
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
64
launcher/ui/Setup/SetupPage.qml
Normal file
64
launcher/ui/Setup/SetupPage.qml
Normal 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
74
launcher/ui/main.qml
Normal 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()
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue