Initial commit

This commit is contained in:
2025-12-17 16:47:48 +00:00
commit 13813f3363
4964 changed files with 1079753 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
add_executable(Bootstrapper
src/main.cpp
resources/qt.qrc
src/Bootstrapper.cpp
src/Bootstrapper.hpp
${CLIENT_DIR}/common/AppSettings.cpp
${CLIENT_DIR}/common/AppSettings.hpp
)
if(AYA_OS_WINDOWS)
target_sources(Bootstrapper PRIVATE
resources/winrc.h
resources/script.rc
)
set_target_properties(Bootstrapper PROPERTIES WIN32_EXECUTABLE TRUE)
windeployqt(Bootstrapper)
endif()
target_compile_definitions(Bootstrapper PRIVATE SKIP_APP_SETTINGS_LOADING)
target_link_libraries(Bootstrapper ${OPENSSL_CRYPTO_LIBRARIES})
target_include_directories(Bootstrapper PRIVATE src resources)
set_target_properties(Bootstrapper PROPERTIES OUTPUT_NAME "Aya")

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -0,0 +1,5 @@
<RCC version="1.0">
<qresource>
<file>icon.ico</file>
</qresource>
</RCC>

View File

@@ -0,0 +1,49 @@
#include "winrc.h"
#if defined(__MINGW64__) || defined(__MINGW32__)
// MinGW-w64, MinGW
#if defined(__has_include) && __has_include(<winres.h>)
#include <winres.h>
#else
#include <afxres.h>
#include <winresrc.h>
#endif
#else
// MSVC, Windows SDK
#include <winres.h>
#endif
IDI_ICON1 ICON APP_ICON
LANGUAGE LANG_ENGLISH, SUBLANG_DEFAULT
VS_VERSION_INFO VERSIONINFO
FILEVERSION VERSION_RESOURCE
PRODUCTVERSION VERSION_RESOURCE
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
#else
FILEFLAGS 0x0L
#endif
FILEOS 0x4L
FILETYPE 0x1L
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0"
BEGIN
VALUE "CompanyName", APP_ORGANIZATION
VALUE "FileDescription", APP_DESCRIPTION
VALUE "FileVersion", VERSION_RESOURCE_STR
VALUE "LegalCopyright", APP_COPYRIGHT
VALUE "ProductName", APP_NAME
VALUE "ProductVersion", VERSION_RESOURCE_STR
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", PRODUCT_LANGUAGE, PRODUCT_CHARSET
END
END

View File

@@ -0,0 +1,24 @@
#pragma once
#define VERSION_MAJOR_MINOR_STR AYA_VERSION_MAJOR_STR "." AYA_VERSION_MINOR_STR
#define VERSION_MAJOR_MINOR_PATCH_STR VERSION_MAJOR_MINOR_STR "." AYA_VERSION_PATCH_STR
#ifdef AYA_VERSION_TYPE
#define VERSION_FULL_STR VERSION_MAJOR_MINOR_PATCH_STR "-" AYA_VERSION_TYPE
#else
#define VERSION_FULL_STR VERSION_MAJOR_MINOR_PATCH_STR
#endif
#define VERSION_RESOURCE AYA_VERSION_MAJOR, AYA_VERSION_MINOR, AYA_VERSION_PATCH, 0
#define VERSION_RESOURCE_STR VERSION_FULL_STR "\0"
/*
* These properties are part of VarFileInfo.
* For more information, please see: https://learn.microsoft.com/en-us/windows/win32/menurc/varfileinfo-block
*/
#define PRODUCT_LANGUAGE 0x0409 // en-US
#define PRODUCT_CHARSET 1200 // Unicode
#define APP_ICON "icon.ico"
#define APP_NAME AYA_PROJECT_NAME "\0"
#define APP_DESCRIPTION AYA_PROJECT_NAME " Bootstrapper\0"
#define APP_ORGANIZATION AYA_PROJECT_NAME "\0"
#define APP_COPYRIGHT AYA_PROJECT_NAME " License\0"

View File

@@ -0,0 +1,308 @@
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include <Windows.h>
#endif
#include "Bootstrapper.hpp"
#include <curl/curl.h>
#include <fstream>
#include <sstream>
#include <iomanip>
#include <iostream>
#include <stdexcept>
#include <vector>
#include <filesystem>
#include <archive.h>
#include <archive_entry.h>
#include <openssl/sha.h>
#include <rapidjson/writer.h>
#include <rapidjson/stringbuffer.h>
size_t WriteToString(void* contents, size_t size, size_t nmemb, void* userp)
{
size_t total = size * nmemb;
auto* out = static_cast<std::string*>(userp);
out->append(static_cast<const char*>(contents), total);
return total;
}
size_t WriteToOStream(void* contents, size_t size, size_t nmemb, void* userp)
{
size_t total = size * nmemb;
auto* os = static_cast<std::ostream*>(userp);
os->write(static_cast<const char*>(contents), static_cast<std::streamsize>(total));
return total;
}
Bootstrapper::Bootstrapper(const std::string& mode, bool showUI, bool forceSkipUpdates, bool isUsingInstance, const std::string& instanceUrl,
const std::string& instanceAccessKey)
: mode(mode)
, showUI(showUI)
, forceSkipUpdates(forceSkipUpdates)
, isUsingInstance(isUsingInstance)
, instanceUrl(instanceUrl)
, instanceAccessKey(instanceAccessKey)
{
}
std::string Bootstrapper::httpGet(const std::string& path)
{
CURL* curl = curl_easy_init();
if (!curl)
throw std::runtime_error("CURL init failed for " + path);
std::string response;
curl_easy_setopt(curl, CURLOPT_URL, (this->instanceUrl + path).c_str());
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteToString);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
struct curl_slist* headers = nullptr;
if (!this->instanceAccessKey.empty())
{
std::string authHeader = "Authorization: " + this->instanceAccessKey;
headers = curl_slist_append(headers, authHeader.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
}
CURLcode res = curl_easy_perform(curl);
if (headers)
curl_slist_free_all(headers);
if (res != CURLE_OK)
{
std::string msg = "CURL GET failed: ";
msg += curl_easy_strerror(res);
curl_easy_cleanup(curl);
throw std::runtime_error(msg);
}
curl_easy_cleanup(curl);
return response;
}
int Bootstrapper::downloadFile(const std::string& path, const std::string& outputPath)
{
CURL* curl = curl_easy_init();
if (!curl)
throw std::runtime_error("CURL init failed for " + path);
std::ofstream ofs(outputPath, std::ios::binary);
if (!ofs)
{
curl_easy_cleanup(curl);
throw std::runtime_error("Failed to open output file: " + outputPath);
}
curl_easy_setopt(curl, CURLOPT_URL, (this->instanceUrl + path).c_str());
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteToOStream);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, static_cast<void*>(&ofs));
struct curl_slist* headers = nullptr;
if (!this->instanceAccessKey.empty())
{
std::string authHeader = "Authorization: " + this->instanceAccessKey;
headers = curl_slist_append(headers, authHeader.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
}
CURLcode res = curl_easy_perform(curl);
curl_off_t downloadSize = 0;
curl_easy_getinfo(curl, CURLINFO_SIZE_DOWNLOAD_T, &downloadSize);
if (headers)
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
if (res != CURLE_OK)
throw std::runtime_error("CURL download failed: " + std::string(curl_easy_strerror(res)));
return static_cast<int>(downloadSize);
}
bool Bootstrapper::verifySHA256(const std::string& filePath, const std::string& expectedHex)
{
std::ifstream file(filePath, std::ios::binary);
if (!file)
throw std::runtime_error("Failed to open file for SHA256: " + filePath);
SHA256_CTX ctx;
SHA256_Init(&ctx);
std::vector<char> buffer(1 << 16);
while (file.good())
{
file.read(buffer.data(), static_cast<std::streamsize>(buffer.size()));
std::streamsize r = file.gcount();
if (r > 0)
SHA256_Update(&ctx, buffer.data(), static_cast<size_t>(r));
}
unsigned char hash[SHA256_DIGEST_LENGTH];
SHA256_Final(hash, &ctx);
std::ostringstream oss;
for (unsigned char b : hash)
{
oss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(b);
}
return oss.str() == expectedHex;
}
void Bootstrapper::extractTarZst(const std::string& archivePath, const std::string& outputDir)
{
struct archive* a = archive_read_new();
archive_read_support_format_tar(a);
archive_read_support_filter_zstd(a);
if (archive_read_open_filename(a, archivePath.c_str(), 10240) != ARCHIVE_OK)
{
std::string err = archive_error_string(a);
archive_read_free(a);
throw std::runtime_error("Failed to open tar.zst: " + err);
}
std::filesystem::create_directories(outputDir);
struct archive_entry* entry;
while (archive_read_next_header(a, &entry) == ARCHIVE_OK)
{
const char* pathname = archive_entry_pathname(entry);
std::filesystem::path outPath = std::filesystem::path(outputDir) / pathname;
if (archive_entry_filetype(entry) == AE_IFDIR)
{
std::filesystem::create_directories(outPath);
}
else
{
std::filesystem::create_directories(outPath.parent_path());
std::ofstream ofs(outPath, std::ios::binary);
const void* buff;
size_t size;
la_int64_t offset;
while (archive_read_data_block(a, &buff, &size, &offset) == ARCHIVE_OK)
{
ofs.write(static_cast<const char*>(buff), static_cast<std::streamsize>(size));
}
}
}
archive_read_free(a);
}
rapidjson::Document Bootstrapper::parseJson(const std::string& jsonStr)
{
rapidjson::Document doc;
doc.Parse(jsonStr.c_str());
if (doc.HasParseError())
throw std::runtime_error("Failed to parse JSON");
return doc;
}
rapidjson::Document Bootstrapper::fetchLatestManifest()
{
return parseJson(httpGet("api/aya/updater/manifest"));
}
rapidjson::Document Bootstrapper::fetchCachedManifest()
{
if (std::filesystem::exists("data/manifest.json"))
{
std::ifstream ifs("data/manifest.json");
std::stringstream buffer;
buffer << ifs.rdbuf();
return parseJson(buffer.str());
}
else
{
// return empty data
if (!std::filesystem::exists("data"))
{
std::filesystem::create_directories("data");
}
std::string filePath = "data/manifest.json";
std::string emptyData = "{}";
std::ofstream ofs(filePath);
ofs << emptyData;
ofs.close();
return parseJson(emptyData);
}
}
void Bootstrapper::updateCachedManifest(const rapidjson::Document& manifest)
{
rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
manifest.Accept(writer);
std::ofstream ofs("data/manifest.json");
ofs << buffer.GetString();
ofs.close();
}
void Bootstrapper::checkForUpdates()
{
rapidjson::Document latestManifest = fetchLatestManifest();
rapidjson::Document cachedManifest = fetchCachedManifest();
if (!cachedManifest.HasMember("version") || cachedManifest["version"].GetString() != latestManifest["version"].GetString())
{
std::string downloadUrl = latestManifest["download_url"].GetString();
std::string expectedSha256 = latestManifest["sha256"].GetString();
std::cout << "New version available: " << latestManifest["version"].GetString() << std::endl;
std::cout << "Downloading from: " << downloadUrl << std::endl;
std::string tempFilePath = "data/update_temp.tar.zst";
downloadFile(downloadUrl, tempFilePath);
if (!verifySHA256(tempFilePath, expectedSha256))
{
throw std::runtime_error("Downloaded file SHA256 does not match expected value.");
}
extractTarZst(tempFilePath, ".");
std::filesystem::remove(tempFilePath);
updateCachedManifest(latestManifest);
std::cout << "Update applied successfully to version " << latestManifest["version"].GetString() << std::endl;
}
else
{
std::cout << "No updates available." << std::endl;
}
}
void launchProcess(const std::string& appName, const std::string& commandLine)
{
std::string fullCommand = appName + " " + commandLine;
int result = std::system(fullCommand.c_str());
if (result != 0)
{
throw std::runtime_error("Failed to launch process: " + fullCommand);
}
}
void Bootstrapper::start(const std::string& commandLine)
{
if (isUsingInstance)
this->checkForUpdates();
if (mode == "player")
launchProcess("Aya.Player", commandLine);
else if (mode == "studio")
launchProcess("Aya.Studio", commandLine);
else if (mode == "server")
launchProcess("Aya.Server", commandLine);
}

View File

@@ -0,0 +1,62 @@
#pragma once
#include <string>
#include <rapidjson/document.h>
#include <QDialog>
#include <QLabel>
#include <QProgressDialog>
#include <QNetworkAccessManager>
#include <QNetworkReply>
class Bootstrapper
{
private:
std::string mode;
bool showUI;
bool forceSkipUpdates;
bool isUsingInstance;
std::string instanceUrl;
std::string instanceAccessKey;
std::string httpGet(const std::string& url);
int downloadFile(const std::string& url, const std::string& outputPath);
rapidjson::Document fetchCachedManifest();
rapidjson::Document fetchLatestManifest();
void updateCachedManifest(const rapidjson::Document& manifest);
static void extractTarZst(const std::string& archivePath, const std::string& outputDir);
static bool verifySHA256(const std::string& filePath, const std::string& expectedHex);
static rapidjson::Document parseJson(const std::string& jsonStr);
public:
Bootstrapper(const std::string& mode, bool showUI, bool forceSkipUpdates, bool isUsingInstance, const std::string& instanceUrl,
const std::string& instanceAccessKey);
void update(const rapidjson::Document& manifest);
void checkForUpdates();
void start(const std::string& commandLine);
};
/*
class BootstrapperProgressDialog : public QDialog
{
Q_OBJECT
public:
explicit BootstrapperProgressDialog();
private slots:
void onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal);
void onFinished();
void onReadyRead();
void onError(QNetworkReply::NetworkError code);
private:
QString formatSize(qint64 bytes) const;
QProgressDialog* progressDialog{nullptr};
QNetworkAccessManager* manager{nullptr};
QNetworkReply* reply{nullptr};
};
*/

View File

@@ -0,0 +1,126 @@
// clang-format off
#include <QApplication>
#include <boost/program_options.hpp>
#include <boost/algorithm/string/join.hpp>
#include <string>
#include <iostream>
#include "AppSettings.hpp"
#include "Bootstrapper.hpp"
#include "winrc.h"
namespace po = boost::program_options;
QCoreApplication* createApplication(int &argc, const char *argv[])
{
for (int i = 1; i < argc; ++i) {
if (!qstrcmp(argv[i], "--no-gui"))
return new QCoreApplication(argc, (char**)argv);
}
return new QApplication(argc, (char**)argv);
}
int main(int argc, const char* argv[])
{
QScopedPointer<QCoreApplication> app(createApplication(argc, argv));
bool showUI = qobject_cast<QApplication*>(app.data()) != nullptr;
QCoreApplication::setOrganizationName(AYA_PROJECT_NAME);
QCoreApplication::setApplicationName(AYA_PROJECT_NAME);
QCoreApplication::setApplicationVersion(VERSION_FULL_STR);
po::options_description desc(AYA_PROJECT_NAME " options");
std::string mode = "player";
bool isUsingInstance = false; // we will determine this through our bespoke methods
bool forceSkipUpdates = false;
std::string appSettingsPath;
std::string instanceUrl, instanceAccessKey;
desc.add_options()
("help,?", "Usage help")
("version,V", "Print version and exit")
("player", "Launch player (default)")
("studio", "Launch studio")
("server", "Launch server")
("skip-updates,F", "Skip update check")
("no-gui", "Run in no-GUI mode (server only)")
("instance-url,U", po::value<std::string>(&instanceUrl), "Instance URL override")
("instance-access-key,k", po::value<std::string>(&instanceAccessKey), "Instance access key")
("app-settings,S", po::value<std::string>(&appSettingsPath)->default_value("AppSettings.ini"), "Path to AppSettings.ini");
po::variables_map vm;
po::store(po::parse_command_line(argc, argv, desc), vm);
po::notify(vm);
if (vm.count("help"))
{
std::cout << desc << std::endl;
return 0;
}
if (vm.count("version"))
{
std::cout << VERSION_FULL_STR << std::endl;
return 0;
}
AppSettings settings(appSettingsPath);
if (!settings.load())
{
std::cout << "Failed to load AppSettings.ini - please make sure it exists with a valid ContentPath under the Aya group, and make sure that it is free of any errors.";
return 0;
}
if (settings.hasGroup("Instance"))
{
if (settings.has("Instance", "Domain"))
if (instanceUrl.empty())
instanceUrl = settings.get("Instance", "Domain").value();
if (settings.has("Instance", "AccessKey"))
if (instanceAccessKey.empty())
instanceAccessKey = settings.get("Instance", "AccessKey").value();
}
isUsingInstance = !instanceUrl.empty();
// legacy holdover
if (isUsingInstance)
if (instanceUrl.rbegin() != instanceUrl.rend() && *instanceUrl.rbegin() != '/')
instanceUrl = instanceUrl + "/";
if (vm.count("studio"))
mode = "studio";
else if (vm.count("server"))
mode = "server";
if (vm.count("skip-updates"))
forceSkipUpdates = true;
std::vector<std::string> args;
for (int i = 1; i < argc; ++i)
{
std::string arg = argv[i];
// Skip bootstrapper-specific arguments
if (arg == "--help" || arg == "-?" ||
arg == "--version" || arg == "-V" ||
arg == "--player" || arg == "--studio" || arg == "--server" ||
arg == "--skip-updates" || arg == "-F")
{
continue;
}
args.push_back(arg);
}
Bootstrapper bootstrapper(mode, showUI, forceSkipUpdates, isUsingInstance, instanceUrl, instanceAccessKey);
bootstrapper.start(boost::algorithm::join(args, " "));
return app->exec();
}