Skip to content

Commit

Permalink
wip: automatic mod updates
Browse files Browse the repository at this point in the history
  • Loading branch information
BenMcAvoy committed Dec 16, 2024
1 parent 8b34dc5 commit 1ca7c13
Show file tree
Hide file tree
Showing 6 changed files with 275 additions and 188 deletions.
29 changes: 14 additions & 15 deletions CarbonLauncher/include/repomanager.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include <vector>
#include <string>
#include <mutex>

constexpr const char* REPOS_URL = "https://github.com/ScrappySM/CarbonLauncher/raw/refs/heads/main/repos.json";

Expand All @@ -18,22 +19,14 @@ namespace Carbon {
// A short description of the mod
std::string description;

// The link to the GitHub repo of the mod
std::string repo;

// A list of all the dependencies of the mod
std::vector<std::string> dependencies;

// The git repo tag to download
std::string tag;

// A list of all the files to download from the GitHub releases
std::vector<std::string> files;

// The supported game version
std::string supported;
// The link to the mods GitHub page
std::string user;
std::string repoName;

bool installed = false;

void Install();
void Uninstall();
};

struct Repo {
Expand All @@ -60,10 +53,16 @@ namespace Carbon {

// Gets all the repos
// @return A vector of all the repos
std::vector<Repo>& GetRepos() { return repos; }
std::vector<Repo>& GetRepos() {
std::lock_guard<std::mutex> lock(this->repoMutex);
return this->repos;
}

bool hasLoaded = false;

private:
Repo JSONToRepo(nlohmann::json json);
std::vector<Repo> repos = {};
std::mutex repoMutex;
};
}; // namespace Carbon
4 changes: 3 additions & 1 deletion CarbonLauncher/src/gamemanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ std::vector<std::string> GameManager::GetLoadedCustomModules() {
// We need to go through all repos and all their mods, incrementing module count
// if one of their files is found in the target process
std::vector<std::string> loadedModules;

/* TODO: Implement this
for (auto& repo : C.repoManager.GetRepos()) {
for (auto& mod : repo.mods) {
for (auto& file : mod.files) {
Expand All @@ -242,7 +244,7 @@ std::vector<std::string> GameManager::GetLoadedCustomModules() {
}
}
}
}
}*/

return loadedModules;
}
65 changes: 10 additions & 55 deletions CarbonLauncher/src/guimanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,13 @@ void _GUI() {
if (ImGui::BeginTabBar("CarbonTabs", ImGuiTabBarFlags_None)) {
// Begin the first tab
if (ImGui::BeginTabItem("Home")) {
if (!C.repoManager.hasLoaded) {
ImGui::TextWrapped("Loading mods...");
ImGui::EndTabItem();
ImGui::End();
return;
}

// Show each installed mod in a child window that spans the entire width of the window
for (auto& repo : C.repoManager.GetRepos()) {
for (auto& mod : repo.mods) {
Expand Down Expand Up @@ -191,14 +198,7 @@ void _GUI() {
return;
}

std::string modulesDir = Utils::GetCurrentModuleDir() + "modules\\";
std::string modFile = modulesDir + repo.name + "\\" + mod.files[0];
std::string tagFile = modulesDir + repo.name + "\\" + mod.files[0].substr(0, mod.files[0].size() - 3) + "tag";

std::filesystem::remove(modFile);
std::filesystem::remove(tagFile);

mod.installed = false;
mod.Uninstall();
}

ImGui::EndChild();
Expand Down Expand Up @@ -288,55 +288,10 @@ void _GUI() {

if (ImGui::Button(mod.installed ? "Uninstall" : "Install")) {
if (mod.installed) {
if (C.gameManager.IsGameRunning()) {
spdlog::error("TODO: Unload the mod from the game (ctx: tried to uninstall mod while game was running)");
return;
}

std::string modulesDir = Utils::GetCurrentModuleDir() + "modules\\";
std::string modFile = modulesDir + repo.name + "\\" + mod.files[0];
std::string tagFile = modulesDir + repo.name + "\\" + mod.files[0].substr(0, mod.files[0].size() - 3) + "tag";

std::filesystem::remove(modFile);
std::filesystem::remove(tagFile);

mod.installed = false;
mod.Uninstall();
}
else {
// Set the mod to installed for now... (we will set it to false if the download fails)
// This is so the UI feels responsive, it shouldn't cause any issues
mod.installed = true;

std::filesystem::create_directory(Utils::GetCurrentModuleDir() + "modules");
std::filesystem::create_directory(Utils::GetCurrentModuleDir() + "modules\\" + repo.name);

std::thread([&]() {
// Download the mods
// TODO: error handling in http
for (auto& file : mod.files) {
std::string url = mod.repo + "/releases/download/" + mod.tag + "/" + file;
std::string path = Utils::GetCurrentModuleDir() + "modules\\" + repo.name + "\\" + file;
cpr::Response response = cpr::Get(cpr::Url{ url });
if (response.status_code != 200) {
mod.installed = false;
return;
}

std::ofstream out(path, std::ios::binary);
out << response.text;
out.close();

std::string fileNoExt = file.substr(0, file.find_last_of('.'));
std::string tagPath = Utils::GetCurrentModuleDir() + "modules\\" + repo.name + "\\" + fileNoExt + ".tag";
std::ofstream tagOut(tagPath);

tagOut << mod.tag;
tagOut.close();
}

// Set the mod as installed
mod.installed = true;
}).detach();
mod.Install();
}
}

Expand Down
188 changes: 156 additions & 32 deletions CarbonLauncher/src/repomanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,91 @@

using namespace Carbon;

// This is here to allow people to not be limited to 60 requests per hour, please don't abuse this
// It has no permissions anyway, so it can't do anything malicious
// It's like this to stop:
// - GitHub from revoking the token because it thinks it's put here by accident
// - Bots scraping the token and using it for malicious purposes
//
// Please, do not abuse this! In the Scrap Mechanic community we trust, right?
constexpr int tok[] = {0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x5f, 0x70, 0x61, 0x74, 0x5f, 0x31, 0x31, 0x42, 0x43, 0x44, 0x32, 0x54, 0x36, 0x51, 0x30, 0x47, 0x45, 0x67, 0x47, 0x5a, 0x50, 0x52, 0x59, 0x7a, 0x32, 0x5a, 0x48, 0x5f, 0x6b, 0x54, 0x70, 0x6c, 0x36, 0x70, 0x48, 0x67, 0x41, 0x59, 0x66, 0x71, 0x70, 0x4e, 0x77, 0x73, 0x4b, 0x73, 0x53, 0x31, 0x69, 0x59, 0x52, 0x51, 0x72, 0x43, 0x59, 0x68, 0x4b, 0x78, 0x6e, 0x6b, 0x72, 0x7a, 0x54, 0x34, 0x71, 0x43, 0x36, 0x61, 0x6d, 0x47, 0x45, 0x33, 0x34, 0x46, 0x57, 0x33, 0x35, 0x37, 0x45, 0x58, 0x7a, 0x77, 0x6b, 0x6c, 0x55, 0x42, 0x6a};
std::string token = std::string(tok, tok + sizeof(tok) / sizeof(tok[0]));

cpr::Header authHeader = cpr::Header{ { "Authorization", "token " + token } };

std::string getDefaultBranch(std::string ghUser, std::string ghRepo) {
auto response = cpr::Get(cpr::Url{ "https://api.github.com/repos/" + ghUser + "/" + ghRepo + "/contents/" }, authHeader);
spdlog::info("Getting default branch for: {}", ghRepo);
auto json = nlohmann::json::parse(response.text);

for (auto& item : json) {
spdlog::info("Checking file: {}", item["name"].get<std::string>());
if (item["download_url"].is_null()) {
continue;
}

// Split on 5th slash to get branch name
std::string downloadUrl = item["download_url"];
std::string branch = downloadUrl.substr(0, downloadUrl.find("/", downloadUrl.find("/", downloadUrl.find("/", downloadUrl.find("/", downloadUrl.find("/") + 1) + 1) + 1) + 1) + 1);

return branch;
}

return "main";
}

Repo RepoManager::JSONToRepo(nlohmann::json json) {
Repo repo;

repo.name = json["name"];
repo.link = json["link"];

for (const auto& mod : json["mods"]) {
Mod m{
.name = mod["name"],
.description = mod["description"],
.repo = mod["repo"],
.tag = mod["tag"],
.supported = mod["supported"]
};

for (const auto& author : mod["authors"]) {
m.authors.push_back(author);
}
std::vector<std::thread> threads;

for (const auto& dependency : mod["dependencies"]) {
m.dependencies.push_back(dependency);
}
for (const auto& jMod : json["mods"]) {
// laughs dubiously
auto handle = std::thread([&]() {
if (jMod.is_null()) {
return;
}

for (const auto& file : mod["files"]) {
std::string sFile = file;
spdlog::info("File: {}", sFile);
m.files.push_back(sFile);
}
spdlog::info("Parsing mod: {}", jMod["name"].get<std::string>());
std::string branch = getDefaultBranch(jMod["ghUser"].get<std::string>(), jMod["ghRepo"].get<std::string>());
std::string manifestURL = fmt::format("https://raw.githubusercontent.com/{}/{}/{}/manifest.json", jMod["ghUser"].get<std::string>(), jMod["ghRepo"].get<std::string>(), branch);
cpr::Response manifest = cpr::Get(cpr::Url{ manifestURL });

bool hasManifest = manifest.status_code == 200;

Mod mod;
mod.user = jMod["ghUser"];
mod.repoName = jMod["ghRepo"];

repo.mods.push_back(m);
if (hasManifest) {
auto jManifest = nlohmann::json::parse(manifest.text);

mod.name = jManifest["name"];
mod.authors = jManifest["authors"].get<std::vector<std::string>>();
mod.description = jManifest["description"];
mod.installed = false;

repo.mods.push_back(mod);
return;
}
else {
mod.name = jMod["name"];
mod.authors = jMod["authors"].get<std::vector<std::string>>();
mod.description = jMod["description"];
mod.installed = false;

repo.mods.push_back(mod);
}
});

threads.push_back(std::move(handle));
}

for (auto& thread : threads) {
thread.join();
}

return repo;
Expand All @@ -56,19 +111,88 @@ std::vector<Repo> RepoManager::URLToRepos(const std::string& url) {
}

RepoManager::RepoManager() {
this->repos = URLToRepos(REPOS_URL);

// Scan the filesystem for all of the `files` inside each mod, if they exist, set `installed` to true
for (auto& repo : this->repos) {
for (auto& mod : repo.mods) {
std::string path = Utils::GetCurrentModuleDir() + "modules\\" + repo.name + "\\" + mod.files[0];
if (std::filesystem::exists(path)) {
mod.installed = true;
}
}
}
//this->repos = URLToRepos(REPOS_URL);S

std::thread([this]() {
// Allow some time for the console to initialize
std::this_thread::sleep_for(std::chrono::milliseconds(200));

std::lock_guard<std::mutex> lock(this->repoMutex);
this->repos = URLToRepos(REPOS_URL);
this->hasLoaded = true;
}).detach();
}

RepoManager::~RepoManager() {
this->repos.clear();
}

void Mod::Install() {
// https://api.github.com/repos/xxx/xxx/releases/latest (.assets[].browser_download_url)
// Default branch: https://api.github.com/repos/xxx/xxx/contents/ (parse .[].name)

//auto contents = cpr::Get(cpr::Url{ "https://api.github.com/repos/" + this->repoName + "/contents/" }, authHeader);
//auto latest = cpr::Get(cpr::Url{ "https://api.github.com/repos/" + this->repoName + "/releases/latest" }, authHeader);

auto contentsURL = fmt::format("https://api.github.com/repos/{}/{}/contents/", this->user, this->repoName);
auto latestURL = fmt::format("https://api.github.com/repos/{}/{}/releases/latest", this->user, this->repoName);
auto contents = cpr::Get(cpr::Url{ contentsURL }, authHeader);
auto latest = cpr::Get(cpr::Url{ latestURL }, authHeader);

auto jContents = nlohmann::json();
auto jLatest = nlohmann::json();

try {
jContents = nlohmann::json::parse(contents.text);
jLatest = nlohmann::json::parse(latest.text);
}
catch (nlohmann::json::parse_error& e) {
spdlog::error("Failed to parse JSON: {}", e.what());
return;
}

// Find the latest tag
std::string tag = jLatest["tag_name"];
spdlog::info("Latest tag: {}", tag);

std::unordered_map<std::string, std::string> downloadURLs;
for (const auto& asset : jLatest["assets"]) {
spdlog::info("Asset: {}", asset.dump(4));

try {
downloadURLs.insert({ asset["name"], asset["browser_download_url"] });
}
catch (nlohmann::json::exception& e) {
spdlog::error("Failed to get browser_download_url: {}", e.what());
continue;
}
}

// Download the mod
for (const auto& [name, url] : downloadURLs) {
auto download = cpr::Get(cpr::Url{ url }, authHeader);

// mod dir
std::string modDir = Utils::GetCurrentModuleDir() + "/mods/" + this->repoName + "/" + this->name + "/";
std::filesystem::create_directories(modDir);

// mod file
std::string modFile = modDir + name;
std::ofstream file(modFile, std::ios::binary);
file << download.text;
file.close();

spdlog::info("Downloaded: {}", modFile);
}

// Save `tag` file with the tag name
std::string tagFile = Utils::GetCurrentModuleDir() + "/mods/" + this->repoName + "/" + this->name + "/tag";
std::ofstream file(tagFile);
file << tag;
file.close();

spdlog::info("Installed: {}", this->name);
}

void Mod::Uninstall() {
}
Loading

0 comments on commit 1ca7c13

Please sign in to comment.