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

Add Watchdog

Helps you get through the queue with instant access to your place,
as well as giving you notifications on lobby errors and what else.

At the moment, only supported under X11 and Linux but will grow in
the future assuming the queues are still terrible.
This commit is contained in:
redstrate 2021-12-06 21:15:31 -05:00
parent b7e22b428c
commit 63faea21cb
11 changed files with 395 additions and 5 deletions

View file

@ -10,7 +10,7 @@ find_package(Qt5 COMPONENTS Core Widgets Network CONFIG REQUIRED)
add_subdirectory(external) add_subdirectory(external)
add_executable(xivlauncher set(SRC
src/main.cpp src/main.cpp
src/launchercore.cpp src/launchercore.cpp
src/sapphirelauncher.cpp src/sapphirelauncher.cpp
@ -21,9 +21,29 @@ add_executable(xivlauncher
src/assetupdater.cpp src/assetupdater.cpp
src/assetupdater.h src/assetupdater.h
src/launcherwindow.cpp src/launcherwindow.cpp
src/launcherwindow.h) src/launcherwindow.h
src/watchdog.h
src/watchdog.cpp)
target_link_libraries(xivlauncher Qt5::Core Qt5::Widgets Qt5::Network qt5keychain QuaZip) set(LIBRARIES
Qt5::Core Qt5::Widgets Qt5::Network qt5keychain QuaZip)
if(UNIX)
set(SRC ${SRC}
src/gameparser.h
src/gameparser.cpp)
set(LIBRARIES ${LIBRARIES}
tesseract
lept
X11
Xcomposite
Xrender)
endif()
add_executable(xivlauncher ${SRC})
target_link_libraries(xivlauncher PUBLIC ${LIBRARIES})
# disgusting, thanks qtkeychain and quazip # disgusting, thanks qtkeychain and quazip
target_include_directories(xivlauncher PRIVATE target_include_directories(xivlauncher PRIVATE

View file

@ -4,7 +4,7 @@ Finally, a cross-platform FFXIV launcher. It should run on Windows, macOS and Li
Compared to XIVQuickLauncher and the official launcher, this supports Compared to XIVQuickLauncher and the official launcher, this supports
**multiple profiles**, **Dalamud mods**, _and_ **macOS and Linux**! This means you no longer **multiple profiles**, **Dalamud mods**, _and_ **macOS and Linux**! This means you no longer
have to suffer running your FFXIV launcher through Wine. have to suffer running your FFXIV launcher through Wine. Under Linux, you have **Watchdog** available to help you through the login queue.
As of this moment, the three missing major features are **game patching**, **external tool launching** and **the news list**. As of this moment, the three missing major features are **game patching**, **external tool launching** and **the news list**.
If you don't use these features then the launcher is still usable. If you don't use these features then the launcher is still usable.
@ -23,6 +23,10 @@ More information can be found in the [FAQ](https://github.com/redstrate/xivlaunc
* Saving username and/or password. These are saved per-profile, and are encrypted using your system wallet. * Saving username and/or password. These are saved per-profile, and are encrypted using your system wallet.
* Encrypted game argument support similiar to what XIVQuickLauncher and the official ffxivboot already does. * Encrypted game argument support similiar to what XIVQuickLauncher and the official ffxivboot already does.
* Enable several (Linux) Wine-specific performance enhancements such as enabling Esync. * Enable several (Linux) Wine-specific performance enhancements such as enabling Esync.
* You have a **Watchdog** available to help you through queues.
* Only works on X11 and Linux (at the moment)
* Will send you a notification on any change in the login queue. (moving up, logged in, lobby error, etc)
* Can view your spot in the queue easily by using the system tray icon.
## Usage ## Usage
Pre-compiled binaries are not (yet) available, so you must compile from source. Please see the relevant Pre-compiled binaries are not (yet) available, so you must compile from source. Please see the relevant

83
src/gameparser.cpp Normal file
View file

@ -0,0 +1,83 @@
#include "gameparser.h"
#include <QDebug>
#include <QRegularExpression>
#include <QBuffer>
GameParser::GameParser() {
api = new tesseract::TessBaseAPI();
if (api->Init(nullptr, "eng")) {
qDebug() << "Could not initialize tesseract!";
return;
}
api->SetPageSegMode(tesseract::PageSegMode::PSM_SINGLE_BLOCK);
}
GameParser::~GameParser() {
api->End();
delete api;
}
GameParseResult GameParser::parseImage(QImage img) {
QBuffer buf;
img = img.convertToFormat(QImage::Format_Grayscale8);
img.save(&buf, "PNG", 100);
Pix* image = pixReadMem((const l_uint8 *) buf.data().data(), buf.size());
api->SetImage(image);
api->SetSourceResolution(300);
const QString text = api->GetUTF8Text();
// TODO: clean up these names
const bool hasWorldFullText = text.contains("This World is currently full.") || text.contains("Players in queue");
const bool hasLobbyErrorText = text.contains("The lobby server connection has encountered an error.");
const bool hasCONFIGURATIONText = text.contains("CONFIGURATION") || text.contains("ONLINE");
const bool hasConnectingToData = text.contains("Connecting to data center");
const bool worldTotallyFull = text.contains("3001");
if(hasLobbyErrorText) {
qDebug() << "LOBBY ERROR";
return {ScreenState::LobbyError, -1};
} else {
if(worldTotallyFull) {
qDebug() << "TOTALLY FULL WORLD (CLOSED BY SQENIX)";
return {ScreenState::WorldFull, -1};
} else {
if(hasConnectingToData) {
qDebug() << "CONNECTING TO DATA CENTER";
return {ScreenState::ConnectingToDataCenter, -1};
} else {
if(hasWorldFullText) {
qDebug() << "FULL WORLD";
// attempt to extract number of players in queue
QRegularExpression exp("(?:Players in queue: )([\\d|,]*)");
auto match = exp.match(text);
if(match.isValid()) {
return {ScreenState::InLoginQueue, match.captured(1).remove(',').toInt()};
}
return {ScreenState::InLoginQueue, -1};
} else {
if(hasCONFIGURATIONText) {
qDebug() << "TITLE SCREEN";
return {ScreenState::EnteredTitleScreen, -1};
}
}
}
}
}
// TODO: figure out how to properly clear tesseract data
api->Clear();
api->ClearAdaptiveClassifier();
return {ScreenState::Splash, -1};
}

39
src/gameparser.h Normal file
View file

@ -0,0 +1,39 @@
#pragma once
#include <QImage>
#include <tesseract/baseapi.h>
#include <leptonica/allheaders.h>
enum class ScreenState {
Splash,
LobbyError,
WorldFull,
ConnectingToDataCenter,
EnteredTitleScreen,
InLoginQueue
};
struct GameParseResult {
ScreenState state;
int playersInQueue = -1;
};
inline bool operator==(const GameParseResult a, const GameParseResult b) {
return a.state == b.state && a.playersInQueue == b.playersInQueue;
}
inline bool operator!=(const GameParseResult a, const GameParseResult b) {
return !(a == b);
}
class GameParser {
public:
GameParser();
~GameParser();
GameParseResult parseImage(QImage image);
private:
tesseract::TessBaseAPI* api;
};

View file

@ -34,6 +34,7 @@
#include "settingswindow.h" #include "settingswindow.h"
#include "blowfish.h" #include "blowfish.h"
#include "assetupdater.h" #include "assetupdater.h"
#include "watchdog.h"
void LauncherCore::setSSL(QNetworkRequest& request) { void LauncherCore::setSSL(QNetworkRequest& request) {
QSslConfiguration config; QSslConfiguration config;
@ -150,7 +151,10 @@ void LauncherCore::launchGame(const ProfileSettings& profile, const LoginAuth au
connect(gameProcess, &QProcess::readyReadStandardOutput, [this, gameProcess, profile] { connect(gameProcess, &QProcess::readyReadStandardOutput, [this, gameProcess, profile] {
QString output = gameProcess->readAllStandardOutput(); QString output = gameProcess->readAllStandardOutput();
qDebug() << "Now launching dalamud...";
auto dalamudProcess = new QProcess(); auto dalamudProcess = new QProcess();
dalamudProcess->setProcessChannelMode(QProcess::ForwardedChannels);
QStringList dalamudEnv = gameProcess->environment(); QStringList dalamudEnv = gameProcess->environment();
@ -300,6 +304,7 @@ void LauncherCore::readInitialInformation() {
profile.useGamemode = settings.value("useGamemode", false).toBool(); profile.useGamemode = settings.value("useGamemode", false).toBool();
profile.useGamescope = settings.value("useGamescope", false).toBool(); profile.useGamescope = settings.value("useGamescope", false).toBool();
profile.enableDXVKhud = settings.value("enableDXVKhud", false).toBool(); profile.enableDXVKhud = settings.value("enableDXVKhud", false).toBool();
profile.enableWatchdog = settings.value("enableWatchdog", false).toBool();
profile.enableDalamud = settings.value("enableDalamud", false).toBool(); profile.enableDalamud = settings.value("enableDalamud", false).toBool();
@ -365,6 +370,7 @@ LauncherCore::LauncherCore() : settings(QSettings::IniFormat, QSettings::UserSco
squareLauncher = new SquareLauncher(*this); squareLauncher = new SquareLauncher(*this);
squareBoot = new SquareBoot(*this, *squareLauncher); squareBoot = new SquareBoot(*this, *squareLauncher);
assetUpdater = new AssetUpdater(*this); assetUpdater = new AssetUpdater(*this);
watchdog = new Watchdog(*this);
readInitialInformation(); readInitialInformation();
@ -375,6 +381,10 @@ LauncherCore::LauncherCore() : settings(QSettings::IniFormat, QSettings::UserSco
connect(squareLauncher, &SquareLauncher::gateStatusRecieved, this, &LauncherCore::settingsChanged); connect(squareLauncher, &SquareLauncher::gateStatusRecieved, this, &LauncherCore::settingsChanged);
} }
LauncherCore::~LauncherCore() noexcept {
delete watchdog;
}
ProfileSettings LauncherCore::getProfile(int index) const { ProfileSettings LauncherCore::getProfile(int index) const {
return profileSettings[index]; return profileSettings[index];
} }
@ -462,6 +472,7 @@ void LauncherCore::saveSettings() {
settings.setValue("rememberPassword", profile.rememberPassword); settings.setValue("rememberPassword", profile.rememberPassword);
settings.setValue("enableDalamud", profile.enableDalamud); settings.setValue("enableDalamud", profile.enableDalamud);
settings.setValue("enableWatchdog", profile.enableWatchdog);
settings.endGroup(); settings.endGroup();
} }

View file

@ -12,6 +12,7 @@ class SapphireLauncher;
class SquareLauncher; class SquareLauncher;
class SquareBoot; class SquareBoot;
class AssetUpdater; class AssetUpdater;
class Watchdog;
struct ProfileSettings { struct ProfileSettings {
QUuid uuid; QUuid uuid;
@ -23,6 +24,7 @@ struct ProfileSettings {
QString bootVersion, gameVersion; QString bootVersion, gameVersion;
int installedMaxExpansion = -1; int installedMaxExpansion = -1;
QList<QString> expansionVersions; QList<QString> expansionVersions;
bool enableWatchdog = false;
// wine // wine
// 0 = system, 1 = custom, 2 = built-in (mac only) // 0 = system, 1 = custom, 2 = built-in (mac only)
@ -63,6 +65,7 @@ class LauncherCore : public QObject {
Q_OBJECT Q_OBJECT
public: public:
LauncherCore(); LauncherCore();
~LauncherCore();
QNetworkAccessManager* mgr; QNetworkAccessManager* mgr;
@ -93,6 +96,7 @@ public:
SquareBoot* squareBoot; SquareBoot* squareBoot;
SquareLauncher* squareLauncher; SquareLauncher* squareLauncher;
AssetUpdater* assetUpdater; AssetUpdater* assetUpdater;
Watchdog* watchdog;
int defaultProfileIndex = 0; int defaultProfileIndex = 0;
signals: signals:

View file

@ -91,6 +91,17 @@ SettingsWindow::SettingsWindow(LauncherWindow& window, LauncherCore& core, QWidg
}); });
gameBoxLayout->addWidget(gameDirectoryButton); gameBoxLayout->addWidget(gameDirectoryButton);
#if defined(Q_OS_LINUX)
enableWatchdog = new QCheckBox("Enable Watchdog (X11 only)");
gameBoxLayout->addWidget(enableWatchdog);
connect(enableWatchdog, &QCheckBox::stateChanged, [this](int state) {
getCurrentProfile().enableWatchdog = state;
this->core.saveSettings();
});
#endif
expansionVersionLabel = new QLabel(); expansionVersionLabel = new QLabel();
gameBoxLayout->addRow("Version Info", expansionVersionLabel); gameBoxLayout->addRow("Version Info", expansionVersionLabel);
@ -343,6 +354,7 @@ void SettingsWindow::reloadControls() {
useEsync->setChecked(profile.useEsync); useEsync->setChecked(profile.useEsync);
useGamescope->setChecked(profile.useGamescope); useGamescope->setChecked(profile.useGamescope);
useGamemode->setChecked(profile.useGamemode); useGamemode->setChecked(profile.useGamemode);
enableWatchdog->setChecked(profile.enableWatchdog);
#endif #endif
// login // login

View file

@ -39,6 +39,7 @@ private:
QLabel* winePrefixDirectory; QLabel* winePrefixDirectory;
QCheckBox* useGamescope, *useEsync, *useGamemode; QCheckBox* useGamescope, *useEsync, *useGamemode;
QCheckBox* enableWatchdog;
// login // login
QCheckBox* encryptArgumentsBox = nullptr; QCheckBox* encryptArgumentsBox = nullptr;

View file

@ -9,6 +9,7 @@
#include <QJsonObject> #include <QJsonObject>
#include "launchercore.h" #include "launchercore.h"
#include "watchdog.h"
SquareLauncher::SquareLauncher(LauncherCore& window) : window(window) { SquareLauncher::SquareLauncher(LauncherCore& window) : window(window) {
@ -123,7 +124,11 @@ void SquareLauncher::registerSession(const LoginInformation& info) {
if(reply->rawHeaderList().contains("X-Patch-Unique-Id")) { if(reply->rawHeaderList().contains("X-Patch-Unique-Id")) {
auth.SID = reply->rawHeader("X-Patch-Unique-Id"); auth.SID = reply->rawHeader("X-Patch-Unique-Id");
if(info.settings->enableWatchdog) {
window.watchdog->launchGame(*info.settings, auth);
} else {
window.launchGame(*info.settings, auth); window.launchGame(*info.settings, auth);
}
} else { } 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."); 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.");
window.addUpdateButtons(*info.settings, *messageBox); window.addUpdateButtons(*info.settings, *messageBox);

180
src/watchdog.cpp Normal file
View file

@ -0,0 +1,180 @@
#include "watchdog.h"
#include <QTimer>
#include <QScreen>
#include <QGuiApplication>
#include <QMenu>
#if defined(Q_OS_LINUX)
#include <X11/X.h>
#include <X11/Xlib.h>
#include <X11/extensions/Xcomposite.h>
#include <X11/extensions/Xrender.h>
// from https://github.com/adobe/webkit/blob/master/Source/WebCore/plugins/qt/QtX11ImageConversion.cpp
// code is licensed under GPLv2
// Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies)
QImage qimageFromXImage(XImage *xi) {
QImage::Format format = QImage::Format_ARGB32_Premultiplied;
if (xi->depth == 24)
format = QImage::Format_RGB32;
else if (xi->depth == 16)
format = QImage::Format_RGB16;
QImage image = QImage(reinterpret_cast<uchar *>(xi->data), xi->width, xi->height, xi->bytes_per_line,
format).copy();
// we may have to swap the byte order
if ((QSysInfo::ByteOrder == QSysInfo::LittleEndian && xi->byte_order == MSBFirst)
|| (QSysInfo::ByteOrder == QSysInfo::BigEndian && xi->byte_order == LSBFirst)) {
for (int i = 0; i < image.height(); i++) {
if (xi->depth == 16) {
ushort *p = reinterpret_cast<ushort *>(image.scanLine(i));
ushort *end = p + image.width();
while (p < end) {
*p = ((*p << 8) & 0xff00) | ((*p >> 8) & 0x00ff);
p++;
}
} else {
uint *p = reinterpret_cast<uint *>(image.scanLine(i));
uint *end = p + image.width();
while (p < end) {
*p = ((*p << 24) & 0xff000000) | ((*p << 8) & 0x00ff0000)
| ((*p >> 8) & 0x0000ff00) | ((*p >> 24) & 0x000000ff);
p++;
}
}
}
}
// fix-up alpha channel
if (format == QImage::Format_RGB32) {
QRgb *p = reinterpret_cast<QRgb *>(image.bits());
for (int y = 0; y < xi->height; ++y) {
for (int x = 0; x < xi->width; ++x)
p[x] |= 0xff000000;
p += xi->bytes_per_line / 4;
}
}
return image;
}
#endif
void Watchdog::launchGame(const ProfileSettings &settings, LoginAuth auth) {
// TODO: stubbed out on other platforms
// (you can't actually enable it on other platforms, so this is fine for now.)
#if defined(Q_OS_LINUX)
if(icon == nullptr) {
icon = new QSystemTrayIcon();
}
icon->setToolTip("Queue Status");
icon->show();
icon->showMessage("Watchdog", "Watchdog service has started. Waiting for you to connect to data center...");
auto timer = new QTimer(this);
auto menu = new QMenu();
auto stopAction = menu->addAction("Stop");
connect(stopAction, &QAction::triggered, [=] {
timer->stop();
processWindowId = -1;
icon->hide();
});
icon->setContextMenu(menu);
core.launchGame(settings, auth);
if(parser == nullptr) {
parser = std::make_unique<GameParser>();
}
connect(timer, &QTimer::timeout, [=] {
if (processWindowId == -1) {
auto xdoProcess = new QProcess();
connect(xdoProcess, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished),
[=](int, QProcess::ExitStatus) {
QString output = xdoProcess->readAllStandardOutput();
qDebug() << "Found XIV Window: " << output.toInt();
processWindowId = output.toInt();
});
// TODO: don't use xdotool for this, find a better way to
xdoProcess->start("bash", {"-c", "xdotool search --name \"FINAL FANTASY XIV\""});
} else {
Display* display = XOpenDisplay(nullptr);
XSynchronize(display, True);
XWindowAttributes attr;
Status status = XGetWindowAttributes(display, processWindowId, &attr);
if (status == 0) {
qDebug() << "Failed to get window attributes! The window is possibly closed now.";
processWindowId = -1;
timer->stop();
icon->hide();
} else {
XCompositeRedirectWindow(display, processWindowId, CompositeRedirectAutomatic);
XCompositeNameWindowPixmap(display, processWindowId);
XRenderPictFormat* format = XRenderFindVisualFormat(display, attr.visual);
XRenderPictureAttributes pa;
pa.subwindow_mode = IncludeInferiors;
Picture picture = XRenderCreatePicture(display, processWindowId, format,
CPSubwindowMode, &pa);
XFlush(display); // TODO: does this actually make a difference?
XImage* image = XGetImage(display, processWindowId, 0, 0, attr.width, attr.height, AllPlanes, ZPixmap);
if (!image) {
qDebug() << "Unable to get image...";
} else {
auto result = parser->parseImage(qimageFromXImage(image));
if (result != lastResult) {
lastResult = result;
switch (result.state) {
case ScreenState::InLoginQueue: {
icon->showMessage("Watchdog",
QString("You are now at position %1 (moved %2 spots)").arg(
result.playersInQueue).arg(
lastResult.playersInQueue - result.playersInQueue));
icon->setToolTip(QString("Queue Status (%1)").arg(result.playersInQueue));
}
break;
case ScreenState::LobbyError: {
// TODO: kill game?
icon->showMessage("Watchdog", "You have been disconnected due to a lobby error.");
}
break;
case ScreenState::ConnectingToDataCenter: {
icon->showMessage("Watchdog",
"You are in the process of being connected to the data center.");
}
break;
case ScreenState::WorldFull: {
icon->showMessage("Watchdog", "You have been disconnected due to a lobby error.");
}
break;
}
}
XFreePixmap(display, picture);
}
}
XCompositeUnredirectWindow(display, processWindowId, CompositeRedirectAutomatic);
}
});
timer->start(5000);
#endif
}

31
src/watchdog.h Normal file
View file

@ -0,0 +1,31 @@
#pragma once
#include <memory>
#include "launchercore.h"
#if defined(Q_OS_LINUX)
#include "gameparser.h"
#endif
#include <QSystemTrayIcon>
class Watchdog : public QObject {
Q_OBJECT
public:
Watchdog(LauncherCore& core) : core(core) {}
void launchGame(const ProfileSettings& settings, LoginAuth auth);
private:
LauncherCore& core;
QSystemTrayIcon* icon = nullptr;
int processWindowId = -1;
#if defined(Q_OS_LINUX)
GameParseResult lastResult;
#endif
std::unique_ptr<GameParser> parser;
};