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

Add initial files

This commit is contained in:
redstrate 2021-11-01 09:54:58 -04:00
parent dfefe7a48c
commit 3ce5e74884
13 changed files with 680 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
*build*/
*.kdev4
.idea/
.DS_Store

25
CMakeLists.txt Executable file
View file

@ -0,0 +1,25 @@
cmake_minimum_required(VERSION 3.0)
project(xivlauncher)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_CXX_STANDARD 17)
find_package(Qt6 COMPONENTS Core Widgets Network CONFIG REQUIRED)
add_subdirectory(external)
add_executable(xivlauncher
src/main.cpp
src/xivlauncher.cpp
src/sapphirelauncher.cpp
src/squareboot.cpp
src/squarelauncher.cpp)
target_link_libraries(xivlauncher Qt6::Core Qt6::Widgets Qt6::Network qt6keychain)
# disgusting, thanks qtkeychain
target_include_directories(xivlauncher PRIVATE
${CMAKE_BINARY_DIR}/_deps/qtkeychain-src
${CMAKE_BINARY_DIR}/_deps/qtkeychain-build)

10
README Executable file
View file

@ -0,0 +1,10 @@
# xivlauncher
Finally, a cross-platform FFXIV launcher. It should run on Windows, macOS (really) and Linux!
This is mostly a hobby project, but should be extremely useful to macOS players who otherwise have to deal with the aging old launcher. Linux users should also appreciate not having to deal with installing XIVQuickLauncher through wine :-)
## Features
* Runs on native (Windows) and Wine (macOS, Linux) versions of FFXIV.
* Can connect to the official Square Enix servers _as well_ as Sapphire servers.
* Saving username and/or password.

12
external/CMakeLists.txt vendored Normal file
View file

@ -0,0 +1,12 @@
include(FetchContent)
FetchContent_Declare(
qtkeychain
GIT_REPOSITORY https://github.com/frankosterfeld/qtkeychain.git
GIT_TAG v0.12.0
)
set(BUILD_WITH_QT6 ON)
set(QTKEYCHAIN_STATIC ON)
FetchContent_MakeAvailable(qtkeychain)

10
src/main.cpp Executable file
View file

@ -0,0 +1,10 @@
#include "xivlauncher.h"
#include <QApplication>
int main(int argc, char* argv[]) {
QApplication app(argc, argv);
LauncherWindow w;
w.show();
return app.exec();
}

69
src/sapphirelauncher.cpp Normal file
View file

@ -0,0 +1,69 @@
#include "sapphirelauncher.h"
#include <QJsonObject>
#include <QJsonDocument>
#include <QMessageBox>
#include <QNetworkReply>
SapphireLauncher::SapphireLauncher(LauncherWindow& window) : window(window) {
}
void SapphireLauncher::login(QString lobbyUrl, const LoginInformation& info) {
QJsonObject data {
{"username", info.username},
{"pass", info.password}
};
QUrl url;
url.setScheme("http");
url.setHost(lobbyUrl);
url.setPath("/sapphire-api/lobby/login");
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader,"application/x-www-form-urlencoded");
auto reply = window.mgr->post(request, QJsonDocument(data).toJson(QJsonDocument::JsonFormat::Compact));
connect(reply, &QNetworkReply::finished, [=] {
QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
if(!document.isEmpty()) {
LoginAuth auth;
auth.SID = document["sId"].toString();
auth.lobbyhost = document["lobbyHost"].toString();
auth.frontierHost = document["frontierHost"].toString();
auth.region = 3;
window.launch(auth);
} else {
auto messageBox = new QMessageBox(QMessageBox::Icon::Critical, "Failed to Login", "Invalid username/password.");
messageBox->show();
}
});
}
void SapphireLauncher::registerAccount(QString lobbyUrl, const LoginInformation& info) {
QJsonObject data {
{"username", info.username},
{"pass", info.password}
};
QUrl url;
url.setScheme("http");
url.setHost(lobbyUrl);
url.setPath("/sapphire-api/lobby/createAccount");
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader,"application/x-www-form-urlencoded");
auto reply = window.mgr->post(request, QJsonDocument(data).toJson(QJsonDocument::JsonFormat::Compact));
connect(reply, &QNetworkReply::finished, [=] {
QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
LoginAuth auth;
auth.SID = document["sId"].toString();
auth.lobbyhost = document["lobbyHost"].toString();
auth.frontierHost = document["frontierHost"].toString();
auth.region = 3;
window.launch(auth);
});
}

16
src/sapphirelauncher.h Normal file
View file

@ -0,0 +1,16 @@
#pragma once
#include <QString>
#include "xivlauncher.h"
class SapphireLauncher : QObject {
public:
SapphireLauncher(LauncherWindow& window);
void login(QString lobbyUrl, const LoginInformation& info);
void registerAccount(QString lobbyUrl, const LoginInformation& info);
private:
LauncherWindow& window;
};

37
src/squareboot.cpp Normal file
View file

@ -0,0 +1,37 @@
#include "squareboot.h"
#include <QUrlQuery>
#include <QNetworkReply>
#include <QMessageBox>
#include "squarelauncher.h"
SquareBoot::SquareBoot(LauncherWindow& window, SquareLauncher& launcher) : window(window), launcher(launcher) {
}
void SquareBoot::bootCheck(LoginInformation& info) {
QUrlQuery query;
query.addQueryItem("time", QDateTime::currentDateTimeUtc().toString("yyyy-MM-dd-HH-mm"));
QUrl url;
url.setScheme("http");
url.setHost("patch-bootver.ffxiv.com");
url.setPath(QString("/http/win32/ffxivneo_release_boot/%1").arg(window.bootVersion));
url.setQuery(query);
auto request = QNetworkRequest(url);
request.setRawHeader("User-Agent", "FFXIV PATCH CLIENT");
request.setRawHeader("Host", "patch-bootver.ffxiv.com");
auto reply = window.mgr->get(request);
connect(reply, &QNetworkReply::finished, [=] {
QString response = reply->readAll();
if(response.isEmpty()) {
launcher.getStored(info);
} else {
auto messageBox = new QMessageBox(QMessageBox::Icon::Critical, "Failed to Login", "Failed to launch. The game may require an update, please use another launcher.");
messageBox->show();
}
});
}

16
src/squareboot.h Normal file
View file

@ -0,0 +1,16 @@
#pragma once
#include "xivlauncher.h"
class SquareLauncher;
class SquareBoot : public QObject {
public:
SquareBoot(LauncherWindow& window, SquareLauncher& launcher);
void bootCheck(LoginInformation& info);
private:
LauncherWindow& window;
SquareLauncher& launcher;
};

157
src/squarelauncher.cpp Normal file
View file

@ -0,0 +1,157 @@
#include "squarelauncher.h"
#include <QFile>
#include <QUrlQuery>
#include <QNetworkReply>
#include <QRegularExpressionMatch>
#include <QMessageBox>
#include "xivlauncher.h"
SquareLauncher::SquareLauncher(LauncherWindow& window) : window(window) {
}
QString getFileHash(QString file) {
auto f = QFile(file);
if (!f.open(QIODevice::ReadOnly))
return "";
QCryptographicHash hash(QCryptographicHash::Sha1);
hash.addData(&f);
return QString("%1/%2").arg(QString::number(f.size()), hash.result().toHex());
}
void SquareLauncher::getStored(const LoginInformation& info) {
QUrlQuery query;
query.addQueryItem("lng", "en");
query.addQueryItem("rgn", "3");
query.addQueryItem("isft", "0");
query.addQueryItem("cssmode", "1");
query.addQueryItem("isnew", "1");
query.addQueryItem("issteam", "0");
QUrl url("https://ffxiv-login.square-enix.com/oauth/ffxivarr/login/top");
url.setQuery(query);
auto request = QNetworkRequest(url);
window.buildRequest(request);
QNetworkReply* reply = window.mgr->get(request);
connect(reply, &QNetworkReply::finished, [=] {
auto str = QString(reply->readAll());
QRegularExpression re(R"lit(\t<\s*input .* name="_STORED_" value="(?<stored>.*)">)lit");
QRegularExpressionMatch match = re.match(str);
if (match.hasMatch()) {
stored = match.captured(1);
login(info, url);
} else {
auto messageBox = new QMessageBox(QMessageBox::Icon::Critical, "Failed to Login", "Failed to contact SE servers. They may be in maintenance.");
messageBox->show();
}
});
}
void SquareLauncher::login(const LoginInformation& info, const QUrl referer) {
QUrlQuery postData;
postData.addQueryItem("_STORED_", stored);
postData.addQueryItem("sqexid", info.username);
postData.addQueryItem("password", info.password);
postData.addQueryItem("otppw", info.oneTimePassword);
QNetworkRequest request(QUrl("https://ffxiv-login.square-enix.com/oauth/ffxivarr/login/login.send"));
window.buildRequest(request);
request.setHeader(QNetworkRequest::ContentTypeHeader,"application/x-www-form-urlencoded");
request.setRawHeader("Referer", referer.toEncoded());
request.setRawHeader("Cache-Control", "no-cache");
auto reply = window.mgr->post(request, postData.toString(QUrl::FullyEncoded).toUtf8());
connect(reply, &QNetworkReply::finished, [=] {
auto str = QString(reply->readAll());
QRegularExpression re(R"lit(window.external.user\("login=auth,ok,(?<launchParams>.*)\);)lit");
QRegularExpressionMatch match = re.match(str);
if(match.hasMatch()) {
const auto parts = match.captured(1).split(',');
const bool terms = parts[3] == "1";
const bool playable = parts[9] == "1";
if(!terms || !playable) {
auto messageBox = new QMessageBox(QMessageBox::Icon::Critical, "Failed to Login", "Your game is unplayable. You may need to accept the terms from the official launcher.");
messageBox->show();
} else {
SID = parts[1];
auth.region = parts[5].toInt();
auth.maxExpansion = parts[13].toInt();
readExpansionVersions(auth.maxExpansion);
registerSession(info);
}
} else {
auto messageBox = new QMessageBox(QMessageBox::Icon::Critical, "Failed to Login", "Invalid username/password.");
messageBox->show();
}
});
}
void SquareLauncher::registerSession(const LoginInformation& info) {
QUrl url;
url.setScheme("https");
url.setHost("patch-gamever.ffxiv.com");
url.setPath(QString("/http/win32/ffxivneo_release_game/%1/%2").arg(window.gameVersion, SID));
auto request = QNetworkRequest(url);
window.setSSL(request);
request.setRawHeader("X-Hash-Check", "enabled");
request.setRawHeader("User-Agent", "FFXIV PATCH CLIENT");
request.setHeader(QNetworkRequest::ContentTypeHeader,"application/x-www-form-urlencoded");
QString report = window.bootVersion + "=" + getBootHash();
for(int i = 0; i < expansionVersions.size(); i++)
report += QString("\nex%1\t%2").arg(QString::number(i + 1), expansionVersions[i]);
auto reply = window.mgr->post(request, report.toUtf8());
connect(reply, &QNetworkReply::finished, [=] {
if(reply->rawHeaderList().contains("X-Patch-Unique-Id")) {
auth.SID = reply->rawHeader("X-Patch-Unique-Id");
window.launch(auth);
} else {
auto messageBox = new QMessageBox(QMessageBox::Icon::Critical, "Failed to Login", "Failed the anti-tamper check. Please restore your game to the original state or update the game.");
messageBox->show();
}
});
}
QString SquareLauncher::getBootHash() {
const QList<QString> fileList =
{
"ffxivboot.exe",
"ffxivboot64.exe",
"ffxivlauncher.exe",
"ffxivlauncher64.exe",
"ffxivupdater.exe",
"ffxivupdater64.exe"
};
QString result;
for (int i = 0; i < fileList.count(); i++) {
result += fileList[i] + "/" + getFileHash(window.gamePath + "/boot/" + fileList[i]);
if (i != fileList.length() - 1)
result += ",";
}
return result;
}
void SquareLauncher::readExpansionVersions(int max) {
for(int i = 0; i < max; i++)
expansionVersions.push_back(window.readVersion(QString("%1/game/sqpack/ex%2/ex%2.ver").arg(window.gamePath, QString::number(i + 1))));
}

25
src/squarelauncher.h Normal file
View file

@ -0,0 +1,25 @@
#pragma once
#include "xivlauncher.h"
class SquareLauncher : public QObject {
public:
SquareLauncher(LauncherWindow& window);
void getStored(const LoginInformation& info);
void login(const LoginInformation& info, const QUrl referer);
void registerSession(const LoginInformation& info);
private:
QString getBootHash();
void readExpansionVersions(int max);
QString stored, SID;
LoginAuth auth;
LauncherWindow& window;
QList<QString> expansionVersions;
};

248
src/xivlauncher.cpp Executable file
View file

@ -0,0 +1,248 @@
#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>
#include "xivlauncher.h"
#include "sapphirelauncher.h"
#include "squarelauncher.h"
#include "squareboot.h"
void LauncherWindow::setSSL(QNetworkRequest& request) {
QSslConfiguration config;
config.setProtocol(QSsl::AnyProtocol);
config.setPeerVerifyMode(QSslSocket::VerifyNone);
request.setSslConfiguration(config);
}
void LauncherWindow::buildRequest(QNetworkRequest& request) {
setSSL(request);
request.setHeader(QNetworkRequest::UserAgentHeader,
QString("SQEXAuthor/2.0.0(Windows 6.2; ja-jp; %1)").arg(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 LauncherWindow::launch(const LoginAuth auth) {
auto process = new QProcess(this);
process->setProcessChannelMode(QProcess::ForwardedChannels);
bool isWine = false;
QString winePath;
QString ffxivPath;
#if defined(Q_OS_WIN)
ffxivPath = gamePath + "\\game\\ffxiv_dx11.exe";
#endif
#if defined(Q_OS_MACOS)
isWine = true;
// TODO: this is assuming FFXIV is installed in /Applications
winePath = "/Applications/FINAL FANTASY XIV ONLINE.app/Contents/SharedSupport/finalfantasyxiv/FINAL FANTASY XIV ONLINE/wine";
ffxivPath = 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/game/ffxiv_dx11.exe";
#endif
#if defined(Q_OS_LINUX)
isWine = true;
// TODO: this is assuming you want to use the wine in your PATH, which isn't always the case
winePath = "wine";
// TODO: this is assuming it's in your default WINEPREFIX
ffxivPath = gamePath + "/game/ffxiv_dx11.exe";
process->setWorkingDirectory(gamePath + "/game/");
#endif
QList<QString> arguments;
if (isWine) {
arguments.push_back(ffxivPath);
}
// i wonder what these mean...
arguments.push_back("DEV.DataPathType=1");
arguments.push_back("DEV.UseSqPack=1");
// by the way, it looks like setting graphics options is possible via these too, i wonder what
// else is hiding :-)))
arguments.push_back(QString("DEV.MaxEntitledExpansionID=%1").arg(auth.maxExpansion));
arguments.push_back(QString("DEV.TestSID=%1").arg(auth.SID));
arguments.push_back(QString("SYS.Region=%1").arg(auth.region));
arguments.push_back(QString("language=%1").arg(language));
arguments.push_back(QString("ver=%1").arg(gameVersion));
if(!auth.lobbyhost.isEmpty()) {
arguments.push_back(QString("DEV.GMServerHost=%1").arg(auth.frontierHost));
for(int i = 1; i < 9; i++)
arguments.push_back(QString("DEV.LobbyHost0%1=%2 DEV.LobbyPort0%1=54994").arg(QString::number(i), auth.lobbyhost));
}
if (isWine) {
process->start(winePath, arguments);
} else {
process->start(ffxivPath, arguments);
}
}
QString LauncherWindow::readVersion(QString path) {
QFile file(path);
file.open(QFile::OpenModeFlag::ReadOnly);
return file.readAll();
}
void LauncherWindow::readInitialInformation() {
#if defined(Q_OS_WIN)
gamePath = "C:\\Program Files (x86\\SquareEnix\\FINAL FANTASY XIV - A Realm Reborn";
#endif
#if defined(Q_OS_MACOS)
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";
#endif
#if defined(Q_OS_LINUX)
// TODO: this is assuming it's in your default WINEPREFIX
gamePath = QDir::homePath() + "/.wine/drive_c/Program Files (x86)/SquareEnix/FINAL FANTASY XIV - A Realm Reborn";
#endif
bootVersion = readVersion(gamePath + "/boot/ffxivboot.ver");
gameVersion = readVersion(gamePath + "/game/ffxivgame.ver");
}
LauncherWindow::LauncherWindow(QWidget* parent) :
QMainWindow(parent) {
mgr = new QNetworkAccessManager();
sapphireLauncher = new SapphireLauncher(*this);
squareLauncher = new SquareLauncher(*this);
squareBoot = new SquareBoot(*this, *squareLauncher);
const auto savedServerType = settings.value("serverType", 0).toInt();
const auto savedLobbyURL = settings.value("lobbyURL", "127.0.0.1").toString();
const auto shouldRememberUsername = settings.value("rememberUsername", false).toBool();
const auto shouldRememberPassword = settings.value("rememberPassword", false).toBool();
auto layout = new QFormLayout();
auto serverType = new QComboBox();
serverType->insertItem(0, "Square Enix");
serverType->insertItem(1, "Sapphire");
serverType->setCurrentIndex(savedServerType);
layout->addRow("Server Lobby", serverType);
auto lobbyServerURL = new QLineEdit();
lobbyServerURL->setText(savedLobbyURL);
layout->addRow("Lobby URL", lobbyServerURL);
auto usernameEdit = new QLineEdit();
layout->addRow("Username", usernameEdit);
if(shouldRememberUsername) {
auto job = new QKeychain::ReadPasswordJob("LauncherWindow");
job->setKey("username");
job->start();
connect(job, &QKeychain::ReadPasswordJob::finished, [=](QKeychain::Job* j) {
usernameEdit->setText(job->textData());
});
}
auto rememberUsernameBox = new QCheckBox();
rememberUsernameBox->setChecked(shouldRememberUsername);
layout->addRow("Remember Username?", rememberUsernameBox);
auto passwordEdit = new QLineEdit();
passwordEdit->setEchoMode(QLineEdit::EchoMode::Password);
layout->addRow("Password", passwordEdit);
if(shouldRememberPassword) {
auto job = new QKeychain::ReadPasswordJob("LauncherWindow");
job->setKey("password");
job->start();
connect(job, &QKeychain::ReadPasswordJob::finished, [=](QKeychain::Job* j) {
passwordEdit->setText(job->textData());
});
}
auto rememberPasswordBox = new QCheckBox();
rememberPasswordBox->setChecked(shouldRememberPassword);
layout->addRow("Remember Password?", rememberPasswordBox);
auto otpEdit = new QLineEdit();
layout->addRow("One-Time Password", otpEdit);
auto loginButton = new QPushButton("Login");
layout->addRow(loginButton);
auto registerButton = new QPushButton("Register");
layout->addRow(registerButton);
const auto refreshControls = [=](int index) {
lobbyServerURL->setEnabled(index == 1);
registerButton->setEnabled(index == 1);
otpEdit->setEnabled(index == 0);
};
refreshControls(serverType->currentIndex());
connect(serverType, &QComboBox::currentIndexChanged, [=](int index) {
refreshControls(index);
});
auto emptyWidget = new QWidget();
emptyWidget->setLayout(layout);
setCentralWidget(emptyWidget);
readInitialInformation();
connect(loginButton, &QPushButton::released, [=] {
auto info = LoginInformation{usernameEdit->text(), passwordEdit->text(), otpEdit->text()};
settings.setValue("rememberUsername", rememberUsernameBox->checkState() == Qt::CheckState::Checked);
if(rememberUsernameBox->checkState() == Qt::CheckState::Checked) {
auto job = new QKeychain::WritePasswordJob("LauncherWindow");
job->setTextData(usernameEdit->text());
job->setKey("username");
job->start();
}
settings.setValue("rememberPassword", rememberPasswordBox->checkState() == Qt::CheckState::Checked);
if(rememberPasswordBox->checkState() == Qt::CheckState::Checked) {
auto job = new QKeychain::WritePasswordJob("LauncherWindow");
job->setTextData(passwordEdit->text());
job->setKey("password");
job->start();
}
settings.setValue("serverType", serverType->currentIndex());
settings.setValue("lobbyURL", lobbyServerURL->text());
if(serverType->currentIndex() == 0) {
// begin se's booting process
squareBoot->bootCheck(info);
} else {
sapphireLauncher->login(lobbyServerURL->text(), info);
}
});
connect(registerButton, &QPushButton::released, [=] {
if(serverType->currentIndex() == 1) {
auto info = LoginInformation{usernameEdit->text(), passwordEdit->text(), otpEdit->text()};
sapphireLauncher->registerAccount(lobbyServerURL->text(), info);
}
});
}
LauncherWindow::~LauncherWindow() = default;

51
src/xivlauncher.h Executable file
View file

@ -0,0 +1,51 @@
#pragma once
#include <QMainWindow>
#include <QNetworkAccessManager>
#include <QFuture>
#include <QSettings>
class SapphireLauncher;
class SquareLauncher;
class SquareBoot;
struct LoginInformation {
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 LauncherWindow : public QMainWindow {
Q_OBJECT
public:
explicit LauncherWindow(QWidget* parent = nullptr);
~LauncherWindow() override;
QNetworkAccessManager* mgr;
int language = 1; // 1 is english, thats all i know
QString gamePath;
QString bootVersion, gameVersion;
void launch(const LoginAuth auth);
void buildRequest(QNetworkRequest& request);
void setSSL(QNetworkRequest& request);
QString readVersion(QString path);
private:
void readInitialInformation();
SapphireLauncher* sapphireLauncher;
SquareBoot* squareBoot;
SquareLauncher* squareLauncher;
QSettings settings;
};