1
Fork 0
mirror of https://github.com/redstrate/Astra.git synced 2025-04-20 11:47:46 +00:00
astra/src/launchercore.cpp

592 lines
19 KiB
C++
Raw Normal View History

2021-11-01 09:54:58 -04:00
#include <QPushButton>
#include <QProcess>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QUrlQuery>
#include <QDir>
#include <QFormLayout>
#include <QLineEdit>
#include <QRegularExpression>
#include <QComboBox>
#include <QJsonObject>
#include <QJsonDocument>
#include <QCheckBox>
#include <keychain.h>
#include <QMessageBox>
2021-11-01 13:14:00 -04:00
#include <QMenuBar>
#include <QCoreApplication>
#include <QStandardPaths>
#include <QRegularExpressionMatch>
2021-11-01 09:54:58 -04:00
2021-11-09 15:02:17 -05:00
#if defined(Q_OS_MAC)
#include <sys/sysctl.h>
#include <mach/mach_time.h>
#endif
#if defined(Q_OS_WIN)
#include <windows.h>
#endif
#include "launchercore.h"
2021-11-01 09:54:58 -04:00
#include "sapphirelauncher.h"
#include "squarelauncher.h"
#include "squareboot.h"
2021-11-01 13:14:00 -04:00
#include "settingswindow.h"
2021-11-09 15:02:17 -05:00
#include "blowfish.h"
#include "assetupdater.h"
2022-01-27 09:25:23 -05:00
#ifdef ENABLE_WATCHDOG
#include "watchdog.h"
2022-01-27 09:25:23 -05:00
#endif
2021-11-01 09:54:58 -04:00
void LauncherCore::setSSL(QNetworkRequest& request) {
2021-11-01 09:54:58 -04:00
QSslConfiguration config;
config.setProtocol(QSsl::AnyProtocol);
config.setPeerVerifyMode(QSslSocket::VerifyNone);
request.setSslConfiguration(config);
}
void LauncherCore::buildRequest(QNetworkRequest& request) {
2021-11-01 09:54:58 -04:00
setSSL(request);
request.setHeader(QNetworkRequest::UserAgentHeader,
QString("SQEXAuthor/2.0.0(Windows 6.2; ja-jp; %1)").arg(QString(QSysInfo::bootUniqueId())));
2021-11-01 09:54:58 -04:00
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");
}
2021-11-09 15:02:17 -05:00
// from xivdev
char ChecksumTable[] = {
'f', 'X', '1', 'p', 'G', 't', 'd', 'S',
'5', 'C', 'A', 'P', '4', '_', 'V', 'L'
};
char GetChecksum(unsigned int key) {
auto value = key & 0x000F0000;
return ChecksumTable[value >> 16];
}
#if defined(Q_OS_MAC)
// this is pretty much what wine does :-0
uint32_t TickCount() {
struct mach_timebase_info convfact;
mach_timebase_info(&convfact);
2021-11-10 05:26:21 -05:00
return (mach_absolute_time() * convfact.numer) / (convfact.denom * 1000000);
2021-11-09 15:02:17 -05:00
}
#endif
2021-11-09 20:59:39 -05:00
#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
2021-11-09 15:02:17 -05:00
QString encryptGameArg(QString arg) {
unsigned int rawTicks = TickCount();
2021-11-09 15:02:17 -05:00
unsigned int ticks = rawTicks & 0xFFFFFFFFu;
2021-11-09 21:32:01 -05:00
unsigned int key = ticks & 0xFFFF0000u;
2021-11-09 15:02:17 -05:00
char buffer[9] = {};
2021-11-09 21:32:01 -05:00
sprintf(buffer, "%08x", key);
2021-11-09 15:02:17 -05:00
Blowfish session(QByteArray(buffer, 8));
QByteArray encryptedArg = session.Encrypt((QString(" /T =%1").arg(ticks) + arg).toUtf8());
QString base64 = encryptedArg.toBase64(QByteArray::Base64Option::Base64UrlEncoding | QByteArray::Base64Option::OmitTrailingEquals);
2021-11-09 21:32:01 -05:00
char checksum = GetChecksum(key);
2021-11-09 21:32:01 -05:00
return QString("//**sqex0003%1%2**//").arg(base64, QString(checksum));
2021-11-09 15:02:17 -05:00
}
void LauncherCore::launchGame(const ProfileSettings& profile, const LoginAuth auth) {
2021-11-02 08:36:30 -04:00
QList<QString> arguments;
QString dataDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
if(profile.enableDalamud) {
arguments.push_back(dataDir + "/NativeLauncher.exe");
}
2021-11-02 08:36:30 -04:00
// now for the actual game...
if(profile.useDX9) {
arguments.push_back(profile.gamePath + "\\game\\ffxiv.exe");
2021-11-02 08:49:06 -04:00
} else {
arguments.push_back(profile.gamePath + "\\game\\ffxiv_dx11.exe");
2021-11-02 08:49:06 -04:00
}
struct Argument {
QString key, value;
};
2021-11-02 08:36:30 -04:00
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.gameVersion});
2021-11-02 08:36:30 -04:00
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)});
}
2021-11-02 08:36:30 -04:00
}
auto gameProcess = new QProcess(this);
if(profile.useSteam) {
gameArgs.push_back({"IsSteam", "1"});
gameProcess->environment() << "IS_FFXIV_LAUNCH_FROM_STEAM=1";
}
gameProcess->setProcessChannelMode(QProcess::MergedChannels);
if(profile.enableDalamud) {
connect(gameProcess, &QProcess::readyReadStandardOutput, [this, gameProcess, profile] {
QString output = gameProcess->readAllStandardOutput();
bool success;
int dec = output.toInt(&success, 10);
if(!success)
return;
qDebug() << "got output: " << output;
qDebug() << "Now launching dalamud...";
auto dalamudProcess = new QProcess();
dalamudProcess->setProcessChannelMode(QProcess::MergedChannels);
QStringList dalamudEnv = gameProcess->environment();
#if defined(Q_OS_LINUX) || defined(Q_OS_MAC)
dalamudEnv << "XL_WINEONLINUX=true";
#endif
dalamudProcess->setEnvironment(dalamudEnv);
QString dataDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
dalamudProcess->start(profile.winePath, {dataDir + "/Dalamud/" + "Dalamud.Injector.exe", output});
});
}
2022-01-27 11:12:23 -05:00
const QString argFormat = profile.encryptArguments ? " /%1 =%2" : "%1=%2";
2021-11-09 15:02:17 -05:00
2022-01-27 11:12:23 -05:00
QString argJoined;
for(const auto& arg : gameArgs) {
argJoined += argFormat.arg(arg.key, arg.value);
}
2022-01-27 11:12:23 -05:00
if(profile.encryptArguments) {
arguments.append(encryptGameArg(argJoined));
} else {
arguments.append(argJoined);
2021-11-09 15:02:17 -05:00
}
2022-01-27 11:12:23 -05:00
connect(gameProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
[=](int exitCode, QProcess::ExitStatus exitStatus){
gameClosed();
});
2022-01-27 11:12:23 -05:00
launchExecutable(profile, gameProcess, arguments);
successfulLaunch();
2021-11-02 08:36:30 -04:00
}
void LauncherCore::launchExecutable(const ProfileSettings& profile, const QStringList args) {
2021-11-01 09:54:58 -04:00
auto process = new QProcess(this);
launchExecutable(profile, process, args);
}
2021-11-01 09:54:58 -04:00
void LauncherCore::launchExecutable(const ProfileSettings& profile, QProcess* process, const QStringList args) {
QList<QString> arguments;
2021-11-02 20:04:53 -04:00
QStringList env = QProcess::systemEnvironment();
2021-11-01 09:54:58 -04:00
#if defined(Q_OS_LINUX)
if(profile.useGamescope) {
arguments.push_back("gamescope");
2022-02-23 21:18:53 -05:00
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));
}
2021-11-01 09:54:58 -04:00
if(profile.useGamemode)
arguments.push_back("gamemoderun");
2021-11-01 09:54:58 -04:00
if(profile.useEsync) {
env << "WINEESYNC=1";
env << "WINEFSYNC=1";
env << "WINEFSYNC_FUTEX2=1";
}
#endif
2021-11-01 09:54:58 -04:00
#if defined(Q_OS_MAC) || defined(Q_OS_LINUX)
env << "WINEPREFIX=" + profile.winePrefixPath;
if(profile.enableDXVKhud)
env << "DXVK_HUD=full";
arguments.push_back(profile.winePath);
#endif
2021-11-02 08:36:30 -04:00
arguments.append(args);
2021-11-01 09:54:58 -04:00
auto executable = arguments[0];
arguments.removeFirst();
process->setWorkingDirectory(profile.gamePath + "/game/");
process->setEnvironment(env);
process->start(executable, arguments);
2021-11-01 09:54:58 -04:00
}
QString LauncherCore::readVersion(QString path) {
2021-11-01 09:54:58 -04:00
QFile file(path);
file.open(QFile::OpenModeFlag::ReadOnly);
return file.readAll();
}
void LauncherCore::readInitialInformation() {
defaultProfileIndex = settings.value("defaultProfile", 0).toInt();
appSettings.closeWhenLaunched = settings.value("closeWhenLaunched", true).toBool();
gamescopeAvailable = checkIfInPath("gamescope");
gamemodeAvailable = checkIfInPath("gamemoderun");
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) {
ProfileSettings profile;
profile.uuid = QUuid(uuid);
settings.beginGroup(uuid);
profile.name = settings.value("name", "Default").toString();
#if defined(Q_OS_MAC)
profile.wineVersion = settings.value("wineVersion", 2).toInt();
#else
profile.wineVersion = settings.value("wineVersion", 0).toInt();
#endif
readWineInfo(profile);
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());
// TODO: UGLY
QString versionString =
doc["targets"]
.toObject()[".NETCoreApp,Version=v5.0"]
.toObject()
.keys()
.filter("Dalamud")[0];
profile.dalamudVersion = versionString.remove("Dalamud/");
}
}
if(settings.contains("gamePath") && settings.value("gamePath").canConvert<QString>() && !settings.value("gamePath").toString().isEmpty()) {
profile.gamePath = settings.value("gamePath").toString();
} else {
2021-11-01 09:54:58 -04:00
#if defined(Q_OS_WIN)
profile.gamePath = "C:\\Program Files (x86)\\SquareEnix\\FINAL FANTASY XIV - A Realm Reborn";
2021-11-01 09:54:58 -04:00
#endif
#if defined(Q_OS_MAC)
profile.gamePath = 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";
2021-11-01 09:54:58 -04:00
#endif
#if defined(Q_OS_LINUX)
profile.gamePath = QDir::homePath() + "/.wine/drive_c/Program Files (x86)/SquareEnix/FINAL FANTASY XIV - A Realm Reborn";
2021-11-01 09:54:58 -04:00
#endif
}
2021-11-01 09:54:58 -04:00
if(settings.contains("winePrefixPath") && settings.value("winePrefixPath").canConvert<QString>() && !settings.value("winePrefixPath").toString().isEmpty()) {
profile.winePrefixPath = settings.value("winePrefixPath").toString();
} else {
#if defined(Q_OS_MACOS)
profile.winePrefixPath = QDir::homePath() + "/Library/Application Support/FINAL FANTASY XIV ONLINE/Bottles/published_Final_Fantasy";
#endif
#if defined(Q_OS_LINUX)
profile.winePrefixPath = QDir::homePath() + "/.wine";
#endif
}
2021-11-09 21:13:21 -05:00
// login
2022-01-27 11:17:45 -05:00
profile.encryptArguments = settings.value("encryptArguments", true).toBool();
2021-11-09 12:40:44 -05:00
profile.isSapphire = settings.value("isSapphire", false).toBool();
2021-11-09 12:50:37 -05:00
profile.lobbyURL = settings.value("lobbyURL", "").toString();
profile.rememberUsername = settings.value("rememberUsername", false).toBool();
profile.rememberPassword = settings.value("rememberPassword", false).toBool();
profile.useSteam = settings.value("useSteam", false).toBool();
2021-11-09 12:16:14 -05:00
profile.useDX9 = settings.value("useDX9", false).toBool();
profile.useEsync = settings.value("useEsync", false).toBool();
if(gamescopeAvailable)
profile.useGamescope = settings.value("useGamescope", false).toBool();
if(gamemodeAvailable)
profile.useGamemode = settings.value("useGamemode", false).toBool();
profile.enableDXVKhud = settings.value("enableDXVKhud", false).toBool();
profile.enableWatchdog = settings.value("enableWatchdog", false).toBool();
2022-02-23 21:18:53 -05:00
// gamescope
profile.gamescope.fullscreen = settings.value("gamescopeFullscreen", true).toBool();
profile.gamescope.borderless = settings.value("gamescopeBorderless", true).toBool();
profile.gamescope.width = settings.value("gamescopeWidth", 0).toInt();
profile.gamescope.height = settings.value("gamescopeHeight", 0).toInt();
profile.gamescope.refreshRate = settings.value("gamescopeRefreshRate", 0).toInt();
profile.enableDalamud = settings.value("enableDalamud", false).toBool();
profileSettings[settings.value("index").toInt()] = profile;
2021-11-02 08:50:14 -04:00
settings.endGroup();
}
readGameVersion();
}
void LauncherCore::readWineInfo(ProfileSettings& profile) {
#if defined(Q_OS_MAC)
switch(profile.wineVersion) {
case 0: // system wine
profile.winePath = "/usr/local/bin/wine64";
break;
case 1: // custom path
profile.winePath = profile.winePath;
break;
case 2: // ffxiv built-in (for mac users)
profile.winePath = "/Applications/FINAL FANTASY XIV ONLINE.app/Contents/SharedSupport/finalfantasyxiv/FINAL FANTASY XIV ONLINE/wine";
break;
}
#endif
#if defined(Q_OS_LINUX)
switch(profile.wineVersion) {
case 0: // system wine (should be in $PATH)
profile.winePath = "wine";
break;
case 1: // custom pth
profile.winePath = profile.winePath;
break;
}
#endif
}
void LauncherCore::readGameVersion() {
for(auto& profile : profileSettings) {
profile.bootVersion = readVersion(profile.gamePath + "/boot/ffxivboot.ver");
profile.gameVersion = readVersion(profile.gamePath + "/game/ffxivgame.ver");
for(auto dir : QDir(profile.gamePath + "/game/sqpack/").entryList(QDir::Filter::Dirs)) {
if(dir.contains("ex") && dir.length() == 3 && dir[2].isDigit()) {
const int expacVersion = dir[2].digitValue();
profile.installedMaxExpansion = std::max(profile.installedMaxExpansion, expacVersion);
}
if(dir == "ffxiv") {
profile.installedMaxExpansion = std::max(profile.installedMaxExpansion, 0);
}
}
readExpansionVersions(profile, profile.installedMaxExpansion);
}
2021-11-01 09:54:58 -04:00
}
LauncherCore::LauncherCore() : settings(QSettings::IniFormat, QSettings::UserScope, QCoreApplication::applicationName()) {
2021-11-01 09:54:58 -04:00
mgr = new QNetworkAccessManager();
sapphireLauncher = new SapphireLauncher(*this);
squareLauncher = new SquareLauncher(*this);
squareBoot = new SquareBoot(*this, *squareLauncher);
assetUpdater = new AssetUpdater(*this);
2022-01-27 09:25:23 -05:00
#ifdef ENABLE_WATCHDOG
watchdog = new Watchdog(*this);
2022-01-27 09:25:23 -05:00
#endif
2021-11-01 09:54:58 -04:00
readInitialInformation();
// check gate status before login
squareLauncher->gateOpen();
// TODO: we really should call this "heavy" signal
connect(squareLauncher, &SquareLauncher::gateStatusRecieved, this, &LauncherCore::settingsChanged);
}
LauncherCore::~LauncherCore() noexcept {
2022-01-27 09:25:23 -05:00
#ifdef ENABLE_WATCHDOG
delete watchdog;
2022-01-27 09:25:23 -05:00
#endif
}
ProfileSettings LauncherCore::getProfile(int index) const {
return profileSettings[index];
}
ProfileSettings& LauncherCore::getProfile(int index) {
return profileSettings[index];
}
int LauncherCore::getProfileIndex(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;
2021-11-09 11:44:27 -05:00
}
int LauncherCore::addProfile() {
2021-11-09 11:44:27 -05:00
ProfileSettings newProfile;
newProfile.uuid = QUuid::createUuid();
2021-11-09 11:44:27 -05:00
newProfile.name = "New Profile";
profileSettings.append(newProfile);
settingsChanged();
return profileSettings.size() - 1;
2021-11-09 12:16:14 -05:00
}
int LauncherCore::deleteProfile(QString name) {
2021-11-09 13:44:37 -05:00
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);
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);
2021-11-09 12:16:14 -05:00
// game
2021-11-09 12:16:14 -05:00
settings.setValue("useDX9", profile.useDX9);
settings.setValue("gamePath", profile.gamePath);
2021-11-09 12:40:44 -05:00
// wine
settings.setValue("wineVersion", profile.wineVersion);
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);
2022-02-23 21:18:53 -05:00
// 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
2021-11-09 21:13:21 -05:00
settings.setValue("encryptArguments", profile.encryptArguments);
2021-11-09 12:40:44 -05:00
settings.setValue("isSapphire", profile.isSapphire);
2021-11-09 12:50:37 -05:00
settings.setValue("lobbyURL", profile.lobbyURL);
settings.setValue("rememberUsername", profile.rememberUsername);
settings.setValue("rememberPassword", profile.rememberPassword);
settings.setValue("useSteam", profile.useSteam);
2021-11-09 12:16:14 -05:00
settings.setValue("enableDalamud", profile.enableDalamud);
settings.setValue("enableWatchdog", profile.enableWatchdog);
2021-11-09 12:16:14 -05:00
settings.endGroup();
}
}
void LauncherCore::addUpdateButtons(const ProfileSettings& settings, QMessageBox& messageBox) {
auto launcherButton = messageBox.addButton("Launch Official Launcher", QMessageBox::NoRole);
connect(launcherButton, &QPushButton::clicked, [=] {
launchExecutable(settings, {settings.gamePath + "/boot/ffxivboot.exe"});
});
messageBox.addButton(QMessageBox::StandardButton::Ok);
}
void LauncherCore::readExpansionVersions(ProfileSettings& info, int max) {
info.expansionVersions.clear();
for(int i = 0; i < max; i++)
info.expansionVersions.push_back(readVersion(QString("%1/game/sqpack/ex%2/ex%2.ver").arg(info.gamePath, QString::number(i + 1))));
}
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();
}