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

Add game installation support

Now Astra can bootstrap a new FFXIV it can't find an existing one. It
doesn't even run the installer, but instead extracts the files from the
installer on the fly using unshield. libxiv is now included to handle
this task.
This commit is contained in:
Joshua Goins 2022-03-16 18:39:13 -04:00
parent a87e6ea271
commit 2bb7b90bec
13 changed files with 70 additions and 258 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@
*.kdev4 *.kdev4
.idea/ .idea/
.DS_Store .DS_Store
.directory

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "external/libxiv"]
path = external/libxiv
url = ../libxiv.git

View file

@ -33,13 +33,11 @@ set(SRC
include/launcherwindow.h include/launcherwindow.h
src/gamescopesettingswindow.cpp src/gamescopesettingswindow.cpp
include/gamescopesettingswindow.h include/gamescopesettingswindow.h
src/fiinparser.cpp
include/fiinparser.h
include/headline.h include/headline.h
src/headline.cpp src/headline.cpp
include/indexparser.h include/config.h
src/indexparser.cpp include/gameinstaller.h
include/config.h) src/gameinstaller.cpp)
include(FetchContent) include(FetchContent)
@ -118,7 +116,7 @@ endif()
add_executable(astra ${SRC}) add_executable(astra ${SRC})
target_link_libraries(astra PUBLIC ${LIBRARIES}) target_link_libraries(astra PUBLIC ${LIBRARIES} libxiv)
target_include_directories(astra target_include_directories(astra
PUBLIC PUBLIC

View file

@ -19,6 +19,7 @@ have more questions, I suggest reading the [FAQ](https://man.sr.ht/~redstrate/as
![screenshot](misc/screenshot.webp?raw=true) ![screenshot](misc/screenshot.webp?raw=true)
## Features ## Features
* Can **bootstrap a new FFXIV installation** if it can't find one. You can skip the installer entirely!
* Can use **native (Windows)** and **Wine-based (macOS, Linux)** versions of FFXIV. * Can use **native (Windows)** and **Wine-based (macOS, Linux)** versions of FFXIV.
* You can use **Dalamud**, which is downloaded within the launcher just like XIVQuickLauncher. * You can use **Dalamud**, which is downloaded within the launcher just like XIVQuickLauncher.
* Can connect to the **official Square Enix servers** _as well_ as **Sapphire servers**. * Can connect to the **official Square Enix servers** _as well_ as **Sapphire servers**.

View file

@ -1,37 +1 @@
include(FetchContent) add_subdirectory(libxiv)
if(NOT TARGET Qt5Keychain::Qt5Keychain)
message("Using built-in qt keychain")
FetchContent_Declare(
qtkeychain
GIT_REPOSITORY https://github.com/frankosterfeld/qtkeychain.git
GIT_TAG v0.12.0
)
set(BUILD_WITH_QT6 OFF CACHE BOOL "" FORCE)
set(QTKEYCHAIN_STATIC ON CACHE BOOL "" FORCE)
set(BUILD_TRANSLATIONS OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(qtkeychain)
set(LIBRARIES qt5keychain ${LIBRARIES} PARENT_SCOPE)
set(KEYCHAIN_INCLUDE_DIRS
${CMAKE_BINARY_DIR}/_deps/qtkeychain-src
${CMAKE_BINARY_DIR}/_deps/qtkeychain-build PARENT_SCOPE)
endif()
if(NOT TARGET QuaZip::QuaZip)
message("Using built-in quazip")
FetchContent_Declare(
quazip
GIT_REPOSITORY https://github.com/stachenov/quazip.git
GIT_TAG v1.2
)
FetchContent_MakeAvailable(quazip)
set(LIBRARIES QuaZip ${LIBRARIES} PARENT_SCOPE)
set(QUAZIP_INCLUDE_DIRS ${CMAKE_BINARY_DIR}/_deps/quazip-src/quazip PARENT_SCOPE)
endif()

1
external/libxiv vendored Submodule

@ -0,0 +1 @@
Subproject commit ffb3350ddc65e8248173716e84e25c7fc5aad271

View file

@ -1,40 +0,0 @@
#pragma once
#include <cstdint>
#include <vector>
#include <string_view>
// this is methods dedicated to parsing "fiin" files, commonly shown as "fileinfo.fiin"
// header is 1024 bytes
// for some reason, they store unknown1 and unknown 2 in this weird format,
// unknown1 is capped at 256 (in decimal) and will overflow into unknown 2
// for example, 1 is equal to unknown1 = 96 and unknown2 = 0
// 96 / 1 == 1
// if you have say, 14 entries, then unknown1 = 64 and unknown2 = 5
// 5 (unknown2) * 256 = 1280 + 64 (unknown1) = 1344
// 1344 / 96 = 14
// i could've made a mistake and this is actually really common but i don't know
struct FileInfoHeader {
char magic[9];
uint8_t dummy1[16];
uint8_t unknown; // version? always seems to be 4
uint8_t dummy2[2];
uint8_t unknown1;
uint8_t unknown2;
uint8_t dummy[994];
};
// each entry is 96 bytes
struct FileInfoEntry {
uint8_t dummy[8]; // length of file name in some format
char str[64]; // simple \0 encoded string
uint8_t dummy2[24]; // sha1
};
struct FileInfo {
FileInfoHeader header;
std::vector<FileInfoEntry> entries;
};
FileInfo readFileInfo(const std::string_view path);

8
include/gameinstaller.h Normal file
View file

@ -0,0 +1,8 @@
#pragma once
#include <QString>
class LauncherCore;
// TODO: convert to a nice signal/slots class like assetupdater
void installGame(LauncherCore& launcher, std::function<void()> returnFunc);

View file

@ -1,62 +0,0 @@
#pragma once
#include <cstdint>
#include <vector>
#include <string_view>
// these are methods dedicated to reading ".index" and ".index2" files
// major thanks to xiv.dev for providing the struct definitions
enum PlatformId : uint8_t
{
Win32,
PS3,
PS4
};
// https://github.com/SapphireServer/Sapphire/blob/develop/deps/datReader/SqPack.cpp#L5
struct SqPackHeader
{
char magic[0x8];
PlatformId platformId;
uint8_t padding0[3];
uint32_t size;
uint32_t version;
uint32_t type;
};
struct SqPackIndexHeader
{
uint32_t size;
uint32_t type;
uint32_t indexDataOffset;
uint32_t indexDataSize;
};
struct IndexHashTableEntry
{
uint64_t hash;
uint32_t unknown : 1;
uint32_t dataFileId : 3;
uint32_t offset : 28;
uint32_t _padding;
};
struct Index2HashTableEntry
{
uint32_t hash;
uint32_t unknown : 1;
uint32_t dataFileId : 3;
uint32_t offset : 28;
};
template<class Entry>
struct IndexFile {
SqPackHeader packHeader;
SqPackIndexHeader indexHeader;
std::vector<Entry> entries;
};
IndexFile<IndexHashTableEntry> readIndexFile(const std::string_view path);
IndexFile<Index2HashTableEntry> readIndex2File(const std::string_view path);

View file

@ -1,47 +0,0 @@
#include "fiinparser.h"
#include <cstdio>
#include <QDebug>
FileInfo readFileInfo(const std::string_view path) {
FILE* file = fopen(path.data(), "rb");
if(!file) {
qInfo() << "Failed to read file info " << path.data();
return {};
}
FileInfo info;
fread(&info.header, sizeof info.header, 1, file);
char magic[9] = "FileInfo";
if(strcmp(info.header.magic, magic) != 0)
qInfo() << "Invalid magic for fileinfo.";
else
qInfo() << "Got matching magic:" << info.header.magic;
qInfo() << "unknown (version?) = " << info.header.unknown;
qInfo() << "unknown1 = " << info.header.unknown1;
qInfo() << "unknown2 = " << info.header.unknown2;
int overflow = info.header.unknown2;
int extra = overflow * 256;
int first = info.header.unknown1 / 96;
int first2 = extra / 96;
int actualEntries = first + first2 + 1; // is this 1 really needed? lol
qInfo() << "Guessed number of entries: " << actualEntries;
int numEntries = actualEntries;
for(int i = 0; i < numEntries; i++) {
FileInfoEntry entry;
fread(&entry, sizeof entry, 1, file);
info.entries.push_back(entry);
qDebug() << entry.str;
}
fclose(file);
return info;
}

34
src/gameinstaller.cpp Normal file
View file

@ -0,0 +1,34 @@
#include "gameinstaller.h"
#include <installextract.h>
#include <QNetworkReply>
#include <QStandardPaths>
#include <QFile>
#include "launchercore.h"
void installGame(LauncherCore& launcher, std::function<void()> returnFunc) {
QString installDirectory = launcher.getProfile(launcher.defaultProfileIndex).gamePath;
qDebug() << "Installing game to " << installDirectory << "!";
qDebug() << "Now downloading installer file...";
QNetworkRequest request(QUrl("https://gdl.square-enix.com/ffxiv/inst/ffxivsetup.exe"));
auto reply = launcher.mgr->get(request);
launcher.connect(reply, &QNetworkReply::finished, [reply, installDirectory, returnFunc] {
QString dataDir =
QStandardPaths::writableLocation(QStandardPaths::TempLocation);
QFile file(dataDir + "/ffxivsetup.exe");
file.open(QIODevice::WriteOnly);
file.write(reply->readAll());
file.close();
extractBootstrapFiles((dataDir + "/ffxivsetup.exe").toStdString(), installDirectory.toStdString());
qDebug() << "Done installing!";
returnFunc();
});
}

View file

@ -1,66 +0,0 @@
#include "indexparser.h"
#include <cstdio>
#include <QDebug>
template<class T>
void commonParseSqPack(FILE* file, IndexFile<T> index) {
fread(&index.packHeader, sizeof index.packHeader, 1, file);
// data starts at size
fseek(file, index.packHeader.size, SEEK_SET);
// read index header
fread(&index.indexHeader, sizeof index.indexHeader, 1, file);
// version should be 1?
qInfo() << index.packHeader.version;
fseek(file, index.indexHeader.indexDataOffset, SEEK_SET);
qInfo() << "size: " << index.indexHeader.indexDataSize;
}
IndexFile<IndexHashTableEntry> readIndexFile(const std::string_view path) {
FILE* file = fopen(path.data(), "rb");
if(!file) {
qInfo() << "Failed to read file info " << path.data();
return {};
}
IndexFile<IndexHashTableEntry> index;
commonParseSqPack(file, index);
for(int i = 0; i < index.indexHeader.indexDataSize; i++) {
IndexHashTableEntry entry;
fread(&entry, sizeof entry, 1, file);
qInfo() << entry.hash;
qInfo() << entry.dataFileId;
qInfo() << entry.offset;
}
return index;
}
IndexFile<Index2HashTableEntry> readIndex2File(const std::string_view path) {
FILE* file = fopen(path.data(), "rb");
if(!file) {
qInfo() << "Failed to read file info " << path.data();
return {};
}
IndexFile<Index2HashTableEntry> index;
commonParseSqPack(file, index);
for(int i = 0; i < index.indexHeader.indexDataSize; i++) {
Index2HashTableEntry entry;
fread(&entry, sizeof entry, 1, file);
qInfo() << entry.hash;
qInfo() << entry.dataFileId;
qInfo() << entry.offset;
}
return index;
}

View file

@ -4,9 +4,11 @@
#include <QApplication> #include <QApplication>
#include <QCommandLineParser> #include <QCommandLineParser>
#include <keychain.h> #include <keychain.h>
#include <QDir>
#include "sapphirelauncher.h" #include "sapphirelauncher.h"
#include "squareboot.h" #include "squareboot.h"
#include "gameinstaller.h"
int main(int argc, char* argv[]) { int main(int argc, char* argv[]) {
QApplication app(argc, argv); QApplication app(argc, argv);
@ -86,6 +88,21 @@ int main(int argc, char* argv[]) {
} }
} }
if(!QDir(c.getProfile(c.defaultProfileIndex).gamePath).exists()) {
auto messageBox = new QMessageBox(QMessageBox::Information, "No Game Found", "No game was found to be installed yet. Would you like to install FFXIV now?");
auto installButton = messageBox->addButton("Install Game", QMessageBox::HelpRole);
c.connect(installButton, &QPushButton::clicked, [&c, messageBox] {
installGame(c, [messageBox] {
messageBox->close();
});
});
messageBox->addButton(QMessageBox::StandardButton::No);
messageBox->show();
}
LauncherWindow w(c); LauncherWindow w(c);
if(!parser.isSet(noguiOption)) { if(!parser.isSet(noguiOption)) {
w.show(); w.show();