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 15, 2024
1 parent 8b34dc5 commit 2e6262a
Show file tree
Hide file tree
Showing 6 changed files with 277 additions and 188 deletions.
31 changes: 16 additions & 15 deletions CarbonLauncher/include/repomanager.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@

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

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

namespace Carbon {
struct Mod {
struct Repo* repo;

// Name of the mode
std::string name;

Expand All @@ -18,22 +21,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 +55,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;
}
119 changes: 64 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,56 +288,65 @@ 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();
}

/*
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;
}
else {
mod.install();
// 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();*/
}

ImGui::EndChild();
Expand Down
134 changes: 102 additions & 32 deletions CarbonLauncher/src/repomanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,84 @@

using namespace Carbon;

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

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()) {
continue;
}

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;

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

Mod mod;
mod.repo = &repo;
mod.name = jManifest["name"];
mod.authors = jManifest["authors"].get<std::vector<std::string>>();
mod.description = jManifest["description"];
mod.user = jMod["ghUser"];
mod.repoName = jMod["ghRepo"];
mod.installed = false;

repo.mods.push_back(mod);
continue;
}
else {
Mod mod;
mod.repo = &repo;
mod.name = jMod["name"];
mod.authors = jMod["authors"].get<std::vector<std::string>>();
mod.description = jMod["description"];
mod.user = jMod["ghUser"];
mod.repoName = jMod["ghRepo"];
mod.installed = false;

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

repo.mods.push_back(m);
threads.push_back(std::move(handle));
}

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

return repo;
Expand All @@ -56,19 +104,41 @@ 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->repo->name + "/contents/" });
auto latest = cpr::Get(cpr::Url{ "https://api.github.com/repos/" + this->repo->name + "/releases/latest" });

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;
}
}

void Mod::Uninstall() {
}
Loading

0 comments on commit 2e6262a

Please sign in to comment.