1
Fork 0
mirror of https://github.com/redstrate/Astra.git synced 2025-04-20 11:47:46 +00:00
astra/src/launchercore.cpp
Joshua Goins 0ef0bca80e Big macOS changes pt. 1
There's a new "Game" menu where you can install the game if you
missed the initial prompt on start-up.

If you have XIV on Mac (a fellow XIV launcher) installed, you
now have the option of using their wine binaries and libraries. There is
more to come in future commits, especially pertaining DXVK and MoltenVK.

Game installation has been fixed for macOS, enabling you to start
updating right away without having to fiddle around with finding the
right installer or app package.
2022-04-14 16:29:23 -04:00

676 lines
24 KiB
C++
Executable file

#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 <QMenuBar>
#include <QCoreApplication>
#include <QStandardPaths>
#include <QRegularExpressionMatch>
#include <algorithm>
#include <QTcpServer>
#include "launchercore.h"
#include "sapphirelauncher.h"
#include "squarelauncher.h"
#include "squareboot.h"
#include "settingswindow.h"
#include "blowfish.h"
#include "assetupdater.h"
#include "encryptedarg.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) {
QList<QString> arguments;
QString dataDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
if(profile.dalamud.enabled) {
arguments.push_back(dataDir + "/NativeLauncher.exe");
arguments.push_back("5248"); // TODO: make port configurable/random
}
// now for the actual game...
if(profile.useDX9) {
arguments.push_back(profile.gamePath + "\\game\\ffxiv.exe");
} else {
arguments.push_back(profile.gamePath + "\\game\\ffxiv_dx11.exe");
}
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.gameVersion});
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)});
}
}
auto gameProcess = new QProcess(this);
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
if(profile.license == GameLicense::WindowsSteam) {
gameArgs.push_back({"IsSteam", "1"});
env.insert("IS_FFXIV_LAUNCH_FROM_STEAM", QString::number(1));
}
if(profile.dalamud.enabled) {
// TODO: this depends on the default wine Z: path existing, which may not
// always the case.
env.insert("DALAMUD_RUNTIME",
"Z:" + dataDir.replace('/', '\\') + "\\DalamudRuntime");
}
gameProcess->setProcessEnvironment(env);
gameProcess->setProcessChannelMode(QProcess::ForwardedChannels);
const QString argFormat = profile.encryptArguments ? " /%1 =%2" : " %1=%2";
QString argJoined;
for(const auto& arg : gameArgs) {
argJoined += argFormat.arg(arg.key, arg.value);
}
if(profile.encryptArguments) {
arguments.append(encryptGameArg(argJoined));
} else {
arguments.append(argJoined);
}
if(profile.dalamud.enabled) {
auto socket = new QTcpServer();
connect(socket, &QTcpServer::newConnection, [this, profile, socket] {
auto connection = socket->nextPendingConnection();
connect(connection, &QTcpSocket::readyRead, [this, connection, profile, socket] {
QString output = connection->readAll();
bool success;
int exitCode = output.toInt(&success, 10);
if(exitCode != -1 && success) {
QString dataDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
dataDir = "Z:" + dataDir.replace('/', '\\');
QJsonObject startInfo;
startInfo["WorkingDirectory"] = dataDir;
startInfo["ConfigurationPath"] = dataDir + "\\dalamudConfig.json";
startInfo["PluginDirectory"] = dataDir + "\\installedPlugins";
startInfo["AssetDirectory"] = dataDir + "\\DalamudAssets";
startInfo["DefaultPluginDirectory"] = dataDir + "\\devPlugins";
startInfo["DelayInitializeMs"] = 0;
startInfo["GameVersion"] = profile.gameVersion;
startInfo["Language"] = profile.language;
startInfo["OptOutMbCollection"] = profile.dalamud.optOutOfMbCollection;
QString argsEncoded = QJsonDocument(startInfo).toJson().toBase64();
auto dalamudProcess = new QProcess();
dalamudProcess->setProcessChannelMode(QProcess::ForwardedChannels);
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 list = dalamudProcess->processEnvironment().toStringList();
launchExecutable(profile, dalamudProcess, {dataDir + "/Dalamud/" + "Dalamud.Injector.exe", QString::number(exitCode), argsEncoded}, false);
connection->close();
socket->close();
}
});
});
socket->listen(QHostAddress::Any, 5248);
}
launchGameExecutable(profile, gameProcess, arguments);
successfulLaunch();
}
void LauncherCore::launchExecutable(const ProfileSettings& profile, const QStringList args) {
auto process = new QProcess(this);
process->setProcessEnvironment(QProcessEnvironment::systemEnvironment());
launchExecutable(profile, process, args, true);
}
void LauncherCore::launchExternalTool(const ProfileSettings& profile, const QStringList args) {
auto process = new QProcess(this);
process->setProcessEnvironment(QProcessEnvironment::systemEnvironment());
launchExecutable(profile, process, args, false);
}
void LauncherCore::launchGameExecutable(const ProfileSettings& profile, QProcess* process, const QStringList args) {
QList<QString> arguments;
arguments.append(args);
launchExecutable(profile, process, arguments, true);
}
void LauncherCore::launchExecutable(const ProfileSettings& profile, QProcess* process, const QStringList args, bool isGame) {
QList<QString> arguments;
auto env = process->processEnvironment();
#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));
}
if (profile.useGamemode)
arguments.push_back("gamemoderun");
}
#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)
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";
env.insert("DYLD_FALLBACK_LIBRARY_PATH", xivLibPath);
}
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);
qDebug() << "launching " << executable << " with args" << arguments;
process->start(executable, arguments);
}
QString LauncherCore::readVersion(QString path) {
QFile file(path);
file.open(QFile::OpenModeFlag::ReadOnly);
return file.readAll();
}
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());
}
}
if(QFile::exists(dataDir + "/nativelauncher.ver")) {
QFile nativeVer(dataDir + "/nativelauncher.ver");
nativeVer.open(QFile::ReadOnly | QFile::Text);
nativeLauncherVersion = QString(nativeVer.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) {
ProfileSettings profile;
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.useOneTimePassword = settings.value("useOneTimePassword", defaultSettings.useOneTimePassword).toBool();
profile.license = (GameLicense)settings.value("license", (int)defaultSettings.license).toInt();
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.fullscreen = settings.value("gamescopeFullscreen", defaultSettings.gamescope.fullscreen).toBool();
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();
wineProcess->setProcessChannelMode(QProcess::MergedChannels);
connect(wineProcess, &QProcess::readyRead, this, [wineProcess, &profile] {
profile.wineVersion = wineProcess->readAllStandardOutput().trimmed();
});
wineProcess->start(profile.winePath, {"--version"});
wineProcess->waitForFinished();
#endif
}
void LauncherCore::readGameVersion() {
for(auto& profile : profileSettings) {
profile.bootVersion = readVersion(profile.gamePath + "/boot/ffxivboot.ver");
profile.gameVersion = readVersion(profile.gamePath + "/game/ffxivgame.ver");
profile.installedMaxExpansion = 0;
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);
}
}
readExpansionVersions(profile, profile.installedMaxExpansion);
}
}
LauncherCore::LauncherCore() : settings(QSettings::IniFormat, QSettings::UserScope, QCoreApplication::applicationName()) {
mgr = new QNetworkAccessManager();
sapphireLauncher = new SapphireLauncher(*this);
squareLauncher = new SquareLauncher(*this);
squareBoot = new SquareBoot(*this, *squareLauncher);
assetUpdater = new AssetUpdater(*this);
#ifdef ENABLE_WATCHDOG
watchdog = new Watchdog(*this);
#endif
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 {
#ifdef ENABLE_WATCHDOG
delete watchdog;
#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;
}
int LauncherCore::addProfile() {
ProfileSettings newProfile;
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(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("useOneTimePassword", profile.useOneTimePassword);
settings.setValue("license", (int)profile.license);
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();
}
}
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();
}
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
}