From 2bb7b90bec7a29f354f9013afcb0ca6850bd7b47 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Wed, 16 Mar 2022 18:39:13 -0400 Subject: [PATCH] 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. --- .gitignore | 1 + .gitmodules | 3 ++ CMakeLists.txt | 10 +++---- README.md | 1 + external/CMakeLists.txt | 38 +----------------------- external/libxiv | 1 + include/fiinparser.h | 40 ------------------------- include/gameinstaller.h | 8 +++++ include/indexparser.h | 62 -------------------------------------- src/fiinparser.cpp | 47 ----------------------------- src/gameinstaller.cpp | 34 +++++++++++++++++++++ src/indexparser.cpp | 66 ----------------------------------------- src/main.cpp | 17 +++++++++++ 13 files changed, 70 insertions(+), 258 deletions(-) create mode 100644 .gitmodules create mode 160000 external/libxiv delete mode 100644 include/fiinparser.h create mode 100644 include/gameinstaller.h delete mode 100644 include/indexparser.h delete mode 100644 src/fiinparser.cpp create mode 100644 src/gameinstaller.cpp delete mode 100644 src/indexparser.cpp diff --git a/.gitignore b/.gitignore index 5cac679..c9eeabb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *.kdev4 .idea/ .DS_Store +.directory diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..db68362 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "external/libxiv"] + path = external/libxiv + url = ../libxiv.git diff --git a/CMakeLists.txt b/CMakeLists.txt index d8a6667..47c6711 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,13 +33,11 @@ set(SRC include/launcherwindow.h src/gamescopesettingswindow.cpp include/gamescopesettingswindow.h - src/fiinparser.cpp - include/fiinparser.h include/headline.h src/headline.cpp - include/indexparser.h - src/indexparser.cpp - include/config.h) + include/config.h + include/gameinstaller.h + src/gameinstaller.cpp) include(FetchContent) @@ -118,7 +116,7 @@ endif() add_executable(astra ${SRC}) -target_link_libraries(astra PUBLIC ${LIBRARIES}) +target_link_libraries(astra PUBLIC ${LIBRARIES} libxiv) target_include_directories(astra PUBLIC diff --git a/README.md b/README.md index 823c33f..2891f9d 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ have more questions, I suggest reading the [FAQ](https://man.sr.ht/~redstrate/as ![screenshot](misc/screenshot.webp?raw=true) ## 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. * 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**. diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index c6f96e6..6b2d568 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -1,37 +1 @@ -include(FetchContent) - -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() +add_subdirectory(libxiv) diff --git a/external/libxiv b/external/libxiv new file mode 160000 index 0000000..ffb3350 --- /dev/null +++ b/external/libxiv @@ -0,0 +1 @@ +Subproject commit ffb3350ddc65e8248173716e84e25c7fc5aad271 diff --git a/include/fiinparser.h b/include/fiinparser.h deleted file mode 100644 index 8e9a939..0000000 --- a/include/fiinparser.h +++ /dev/null @@ -1,40 +0,0 @@ -#pragma once - -#include -#include -#include - -// 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 entries; -}; - -FileInfo readFileInfo(const std::string_view path); \ No newline at end of file diff --git a/include/gameinstaller.h b/include/gameinstaller.h new file mode 100644 index 0000000..7329b7a --- /dev/null +++ b/include/gameinstaller.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +class LauncherCore; + +// TODO: convert to a nice signal/slots class like assetupdater +void installGame(LauncherCore& launcher, std::function returnFunc); \ No newline at end of file diff --git a/include/indexparser.h b/include/indexparser.h deleted file mode 100644 index 6c506e5..0000000 --- a/include/indexparser.h +++ /dev/null @@ -1,62 +0,0 @@ -#pragma once - -#include -#include -#include - -// 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 -struct IndexFile { - SqPackHeader packHeader; - SqPackIndexHeader indexHeader; - - std::vector entries; -}; - -IndexFile readIndexFile(const std::string_view path); -IndexFile readIndex2File(const std::string_view path); \ No newline at end of file diff --git a/src/fiinparser.cpp b/src/fiinparser.cpp deleted file mode 100644 index 5bbced8..0000000 --- a/src/fiinparser.cpp +++ /dev/null @@ -1,47 +0,0 @@ -#include "fiinparser.h" - -#include -#include - -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; -} \ No newline at end of file diff --git a/src/gameinstaller.cpp b/src/gameinstaller.cpp new file mode 100644 index 0000000..e439150 --- /dev/null +++ b/src/gameinstaller.cpp @@ -0,0 +1,34 @@ +#include "gameinstaller.h" + +#include +#include +#include +#include + +#include "launchercore.h" + +void installGame(LauncherCore& launcher, std::function 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(); + }); +} \ No newline at end of file diff --git a/src/indexparser.cpp b/src/indexparser.cpp deleted file mode 100644 index f5095eb..0000000 --- a/src/indexparser.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include "indexparser.h" - -#include -#include - -template -void commonParseSqPack(FILE* file, IndexFile 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 readIndexFile(const std::string_view path) { - FILE* file = fopen(path.data(), "rb"); - if(!file) { - qInfo() << "Failed to read file info " << path.data(); - return {}; - } - - IndexFile 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 readIndex2File(const std::string_view path) { - FILE* file = fopen(path.data(), "rb"); - if(!file) { - qInfo() << "Failed to read file info " << path.data(); - return {}; - } - - IndexFile 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; -} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 8b8ad9f..37b7cf7 100755 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,9 +4,11 @@ #include #include #include +#include #include "sapphirelauncher.h" #include "squareboot.h" +#include "gameinstaller.h" int main(int argc, char* 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); if(!parser.isSet(noguiOption)) { w.show();