diff --git a/CarbonLauncher/include/repomanager.h b/CarbonLauncher/include/repomanager.h index 82cdfbd..dabda1f 100644 --- a/CarbonLauncher/include/repomanager.h +++ b/CarbonLauncher/include/repomanager.h @@ -4,11 +4,14 @@ #include #include +#include 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; @@ -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 dependencies; - - // The git repo tag to download - std::string tag; - - // A list of all the files to download from the GitHub releases - std::vector 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 { @@ -60,10 +55,16 @@ namespace Carbon { // Gets all the repos // @return A vector of all the repos - std::vector& GetRepos() { return repos; } + std::vector& GetRepos() { + std::lock_guard lock(this->repoMutex); + return this->repos; + } + + bool hasLoaded = false; private: Repo JSONToRepo(nlohmann::json json); std::vector repos = {}; + std::mutex repoMutex; }; }; // namespace Carbon diff --git a/CarbonLauncher/src/gamemanager.cpp b/CarbonLauncher/src/gamemanager.cpp index f7a501e..71b1028 100644 --- a/CarbonLauncher/src/gamemanager.cpp +++ b/CarbonLauncher/src/gamemanager.cpp @@ -234,6 +234,8 @@ std::vector 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 loadedModules; + + /* TODO: Implement this for (auto& repo : C.repoManager.GetRepos()) { for (auto& mod : repo.mods) { for (auto& file : mod.files) { @@ -242,7 +244,7 @@ std::vector GameManager::GetLoadedCustomModules() { } } } - } + }*/ return loadedModules; } diff --git a/CarbonLauncher/src/guimanager.cpp b/CarbonLauncher/src/guimanager.cpp index e39c852..b9d356c 100644 --- a/CarbonLauncher/src/guimanager.cpp +++ b/CarbonLauncher/src/guimanager.cpp @@ -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) { @@ -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(); @@ -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(); diff --git a/CarbonLauncher/src/repomanager.cpp b/CarbonLauncher/src/repomanager.cpp index 41c2ae7..b9ace1b 100644 --- a/CarbonLauncher/src/repomanager.cpp +++ b/CarbonLauncher/src/repomanager.cpp @@ -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()); + 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 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 branch = getDefaultBranch(jMod["ghUser"].get(), jMod["ghRepo"].get()); + std::string manifestURL = fmt::format("https://raw.githubusercontent.com/{}/{}/{}/manifest.json", jMod["ghUser"].get(), jMod["ghRepo"].get(), 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>(); + 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>(); + 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; @@ -56,19 +104,41 @@ std::vector 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 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() { +} diff --git a/repos.json b/repos.json index a3a1f8c..5efeaf5 100644 --- a/repos.json +++ b/repos.json @@ -1,93 +1,73 @@ { - "repositories": [ + "repositories": [ + { + "name": "Scrap-Mods", + "link": "https://github.com/Scrap-Mod", + "mods": [ { - "name": "Scrap-Mods", - "link": "https://github.com/Scrap-Mod", - "mods": [ - { - "name": "Networking Fix", - "authors": [ - "QuestionableM", - "ColdMeekly" - ], - "description": "Stops Scrap Mechanic client from stalling packets. Fixes pretty much all the networking issues.", - "repo": "https://github.com/Scrap-Mods/networking-fix", - "dependencies": [], - "tag": "v1.0.3", - "files": [ - "NetworkingFix.dll" - ], - "supported": "v0.7.2.775" - } - ] + "name": "Networking Fix", + "description": "Stops Scrap Mechanic client from stalling packets. Fixes pretty much all the networking issues.", + "authors": [ + "QuestionableM", + "ColdMeekly" + ], + + "ghUser": "Scrap-Mods", + "ghRepo": "Networking-Fix" + } + ] + }, + { + "name": "QuestionableM", + "link": "https://github.com/QuestionableM", + "mods": [ + { + "name": "Proximity Voice Chat", + "description": "A Scrap Mechanic DLL mod which adds the Proximity Voice Chat into the game", + "authors": [ + "QuestionableM" + ], + + "ghUser": "QuestionableM", + "ghRepo": "SM-ProximityVoiceChat" }, { - "name": "QuestionableM", - "link": "https://github.com/QuestionableM", - "mods": [ - { - "name": "Proximity Voice Chat", - "authors": [ - "QuestionableM" - ], - "description": "A Scrap Mechanic DLL mod which adds the Proximity Voice Chat into the game", - "repo": "https://github.com/QuestionableM/SM-ProximityVoiceChat", - "dependencies": [], - "tag": "v1.3.3", - "files": [ - "SM-ProximityVoiceChat.dll" - ], - "supported": "v0.7.2.775" - }, - { - "name": "Better Paint Tool", - "authors": [ - "QuestionableM" - ], - "description": "A DLL mod for Scrap Mechanic which enhances the functionality of the vanilla Paint Tool and allows you to pick any color you want!", - "repo": "https://github.com/QuestionableM/SM-BetterPaintTool", - "dependencies": [], - "tag": "v1.5.4", - "files": [ - "SM-BetterPaintTool.dll" - ], - "supported": "v0.7.2.775" - }, - { - "name": "Dynamic Sun", - "authors": [ - "QuestionableM" - ], - "description": "A Scrap Mechanic DLL mod which makes the sun dynamic!", - "repo": "https://github.com/QuestionableM/SM-DynamicSun", - "dependencies": [], - "tag": "v1.1.4", - "files": [ - "DynamicSun.dll" - ], - "supported": "v0.7.2.775" - } - ] + "name": "Better Paint Tool", + "description": "A DLL mod for Scrap Mechanic which enhances the functionality of the vanilla Paint Tool and allows you to pick any color you want!", + "authors": [ + "QuestionableM" + ], + + "ghUser": "QuestionableM", + "ghRepo": "SM-BetterPaintTool" }, { - "name": "VeraDev0", - "link": "https://github.com/VeraDev0", - "mods": [ - { - "name": "No Auto Smart Physics", - "authors": [ - "VeraDev0" - ], - "description": "Prevent Scrap Mechanic from automaticly changing your physics quality to Smart if too laggy.", - "repo": "https://github.com/VeraDev0/SM-NoAutoSmartPhysics", - "dependencies": [], - "tag": "v1.1", - "files": [ - "SM-NoAutoSmartPhysics.dll" - ], - "supported": "v0.7.2.775" - } - ] + "name": "Dynamic Sun", + "description": "A Scrap Mechanic DLL mod which makes the sun dynamic!", + "authors": [ + "QuestionableM" + ], + + "ghUser": "QuestionableM", + "ghRepo": "SM-DynamicSun" + } + ] + }, + { + "name": "VeraDev0", + "link": "https://github.com/VeraDev0", + "mods": [ + { + "name": "No Auto Smart Physics", + "description": "Prevent Scrap Mechanic from automaticly changing your physics quality to Smart if too laggy.", + "authors": [ + "VeraDev0" + ], + + "ghUser": "VeraDev0", + "ghRepo": "SM-NoAutoSmartPhysics" } - ] + ] + } + ] } diff --git a/schema.json b/schema.json new file mode 100644 index 0000000..28f3715 --- /dev/null +++ b/schema.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "authors": { + "type": "array", + "items": [ + { + "type": "string" + } + ] + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "name", + "description" + ] +}