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:
parent
a87e6ea271
commit
2bb7b90bec
13 changed files with 70 additions and 258 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@
|
||||||
*.kdev4
|
*.kdev4
|
||||||
.idea/
|
.idea/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.directory
|
||||||
|
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "external/libxiv"]
|
||||||
|
path = external/libxiv
|
||||||
|
url = ../libxiv.git
|
|
@ -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
|
||||||
|
|
|
@ -19,6 +19,7 @@ have more questions, I suggest reading the [FAQ](https://man.sr.ht/~redstrate/as
|
||||||

|

|
||||||
|
|
||||||
## 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**.
|
||||||
|
|
38
external/CMakeLists.txt
vendored
38
external/CMakeLists.txt
vendored
|
@ -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
1
external/libxiv
vendored
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit ffb3350ddc65e8248173716e84e25c7fc5aad271
|
|
@ -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
8
include/gameinstaller.h
Normal 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);
|
|
@ -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);
|
|
|
@ -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
34
src/gameinstaller.cpp
Normal 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();
|
||||||
|
});
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
17
src/main.cpp
17
src/main.cpp
|
@ -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();
|
||||||
|
|
Loading…
Add table
Reference in a new issue