LiteLoaderBDS-1.16.40/LiteLoader/Main/AddonsHelper.cpp
2022-10-29 01:12:57 -07:00

677 lines
26 KiB
C++

#include "AddonsHelper.h"
#include <Main/Config.h>
#include <EventAPI.h>
#include <RegCommandAPI.h>
#include <LLAPI.h>
#include <MC/CommandOrigin.hpp>
#include <MC/CommandOutput.hpp>
#include <MC/CommandPosition.hpp>
#include <MC/CommandRegistry.hpp>
#include <MC/PropertiesSettings.hpp>
#include <MC/Level.hpp>
#include <GlobalServiceAPI.h>
#include <Utils/WinHelper.h>
#include <Utils/FileHelper.h>
#include <Nlohmann/json.hpp>
#include <Main/Config.h>
#include <LoggerAPI.h>
#include <I18nAPI.h>
#include <filesystem>
#include <set>
#include <vector>
#include <MC/ColorFormat.hpp>
#include <magic_enum/magic_enum.hpp>
using namespace std;
using namespace RegisterCommandHelper;
Logger addonLogger("AddonHelper");
std::vector<Addon> addons;
bool AutoInstallAddons(string path);
// Helper
std::string GetAddonJsonFile(Addon::Type type) {
string addonListFile = Level::getCurrentLevelPath();
switch (type) {
case Addon::Type::BehaviorPack:
return addonListFile + "/world_behavior_packs.json";
break;
case Addon::Type::ResourcePack:
return addonListFile + "/world_resource_packs.json";
break;
default:
break;
}
return "";
}
inline bool isManifestFile(std::string const& filename) {
return filename == "manifest.json" || filename == "pack_manifest.json";
}
#include <MC/JsonHelpers.hpp>
inline std::string FixMojangJson(std::string const& content) {
Json::Value value;
JsonHelpers::parseJson(content, value);
return JsonHelpers::serialize(value);
}
std::optional<Addon> parseAddonFromPath(std::filesystem::path addonPath) {
try {
auto manifestPath = addonPath;
manifestPath.append("manifest.json");
if (!filesystem::exists(manifestPath)) {
manifestPath = addonPath;
manifestPath.append("pack_manifest.json");
}
auto manifestFile = ReadAllFile(UTF82String(manifestPath.u8string()));
if (!manifestFile || manifestFile->empty())
throw std::exception("manifest.json not found!");
std::string content = FixMojangJson(*manifestFile);
auto manifest = nlohmann::json::parse(content, nullptr, true, true);
auto header = manifest["header"];
auto uuid = header["uuid"];
Addon addon;
addon.name = header["name"];
addon.description = header["description"];
addon.uuid = uuid;
addon.directory = UTF82String(addonPath.u8string());
auto ver = header["version"];
addon.version = LL::Version(ver[0], ver[1], ver[2]);
string type = manifest["modules"][0]["type"];
if (type == "resources")
addon.type = Addon::Type::ResourcePack;
else if (type == "data" || type == "script")
addon.type = Addon::Type::BehaviorPack;
else
throw std::exception("Unknown type of addon pack!");
return addon;
} catch (const seh_exception& e) {
addonLogger.error("Uncaught SEH Exception Detected!");
addonLogger.error("In " __FUNCTION__ " " + UTF82String(addonPath.u8string()));
addonLogger.error("Error: Code[{}] {}", e.code(), TextEncoding::toUTF8(e.what()));
} catch (const std::exception& e) {
addonLogger.error("Uncaught C++ Exception Detected!");
addonLogger.error("In " __FUNCTION__ " " + UTF82String(addonPath.u8string()));
addonLogger.error("Error: Code[{}] {}", -1, TextEncoding::toUTF8(e.what()));
} catch (...) {
addonLogger.error("Uncaught Exception Detected!");
addonLogger.error("In " __FUNCTION__ " " + UTF82String(addonPath.u8string()));
}
return std::nullopt;
}
bool RemoveAddonFromList(Addon& addon) {
auto jsonFile = GetAddonJsonFile(addon.type);
if (!filesystem::exists(str2wstr(jsonFile))) {
addonLogger.error(tr("ll.addonsHelper.error.addonConfigNotFound"));
return false;
}
auto addonJsonContent = ReadAllFile(jsonFile);
if (!addonJsonContent || addonJsonContent->empty()) {
addonLogger.error(tr("ll.addonsHelper.error.addonConfigNotFound"));
return false;
}
auto addonJson = fifo_json::parse(*addonJsonContent, nullptr, true, true);
int id = 0;
for (auto item : addonJson) {
if (item["pack_id"] == addon.uuid) {
addonJson.erase(id);
bool res = WriteAllFile(jsonFile, addonJson.dump(4));
if (!res) {
addonLogger.error(tr("ll.addonsHelper.removeAddonFromList.fail", addon.getPrintName()));
return false;
}
addonLogger.info(tr("ll.addonsHelper.removeAddonFromList.success", addon.getPrintName()));
return true;
}
++id;
}
addonLogger.error(tr("ll.addonsHelper.error.addonNotFound", addon.getPrintName()));
return false;
}
bool AddAddonToList(Addon& addon) {
string addonListFile = GetAddonJsonFile(addon.type);
if (!filesystem::exists(str2wstr(addonListFile))) {
ofstream fout(addonListFile);
fout << "[]" << flush;
}
try {
bool exists = false;
auto addonList = nlohmann::json::parse(*ReadAllFile(addonListFile), nullptr, false, true);
// Auto fix Addon List File
if (!addonList.is_array()) {
auto backupPath = UTF82String(filesystem::path(str2wstr(addonListFile)).stem().u8string()) + "_error.json";
addonLogger.error(tr("ll.addonsHelper.addAddonToList.invalidList", addonListFile, backupPath));
std::error_code ec;
std::filesystem::rename(str2wstr(addonListFile), filesystem::path(addonListFile).remove_filename().append(str2wstr(backupPath)), ec);
addonList = "[]"_json;
}
for (auto& addonData : addonList) {
if (addonData["pack_id"] == addon.uuid) {
addonData["version"] = {addon.version.major, addon.version.minor, addon.version.revision};
exists = true;
break;
}
}
if (!exists) {
auto newAddonData = nlohmann::json::object();
newAddonData["pack_id"] = addon.uuid;
newAddonData["version"] = {addon.version.major, addon.version.minor, addon.version.revision};
addonList.push_back(newAddonData);
}
bool res = WriteAllFile(addonListFile, addonList.dump(4));
if (!res)
throw std::exception("Fail to write data back to addon list file!");
addonLogger.info(tr("ll.addonsHelper.addAddonToList.success", addon.getPrintName()));
return true;
} catch (const std::exception& e) {
addonLogger.error(tr("ll.addonsHelper.addAddonToList.fail", addon.getPrintName(), addonListFile));
addonLogger.error(tr("ll.addonsHelper.displayError", TextEncoding::toUTF8(e.what())));
addonLogger.error(tr("ll.addonsHelper.error.installationAborted"));
return false;
}
}
bool InstallAddonToLevel(std::string addonDir, std::string addonName) {
auto addon = parseAddonFromPath(str2wstr(addonDir));
if (!addon.has_value())
return false;
std::string subPath;
if (addon->type == Addon::Type::ResourcePack)
subPath = "/resource_packs";
else if (addon->type == Addon::Type::BehaviorPack)
subPath = "/behavior_packs";
// copy files
string levelPath = Level::getCurrentLevelPath();
string toPath = levelPath + subPath + "/" + addonName;
// Avoid duplicate names or update addon if same uuid
while (filesystem::exists(str2wstr(toPath))) {
auto tmp = parseAddonFromPath(str2wstr(toPath));
if (tmp.has_value() && tmp->uuid != addon->uuid) {
toPath += "_";
} else {
std::error_code ec;
filesystem::remove_all(str2wstr(toPath), ec);
break;
}
}
std::error_code ec;
filesystem::create_directories(str2wstr(toPath), ec);
filesystem::copy(str2wstr(addonDir), str2wstr(toPath), filesystem::copy_options::recursive, ec);
// add addon to list file
return AddAddonToList(*addon);
}
void FindManifest(vector<string>& result, const string& path) {
filesystem::directory_iterator ent(str2wstr(path));
bool foundManifest = false;
for (auto& file : ent) {
auto path = file.path();
if (isManifestFile(UTF82String(path.filename().u8string()))) {
result.push_back(UTF82String(filesystem::canonical(path).parent_path().u8string()));
foundManifest = true;
break;
}
}
if (!foundManifest) {
// No manifest file
if (!AutoInstallAddons(path)) {
filesystem::directory_iterator ent2(str2wstr(path));
for (auto& file : ent2)
if (file.is_directory())
FindManifest(result, UTF82String(file.path().u8string()));
}
}
return;
}
std::string Addon::getPrintName() const {
if (LL::globalConfig.colorLog)
return ColorFormat::convertToConsole(std::string(name));
else
return ColorFormat::removeColorCode(std::string(name));
}
bool AddonsManager::install(std::string packPath) {
try {
if (!filesystem::exists(str2wstr(packPath))) {
addonLogger.error(tr("ll.addonsHelper.error.addonFileNotFound", packPath));
return false;
}
if (VALID_ADDON_FILE_EXTENSION.find(UTF82String(filesystem::path(str2wstr(packPath)).extension().u8string())) == VALID_ADDON_FILE_EXTENSION.end()) {
addonLogger.error(tr("ll.addonsHelper.error.unsupportedFileType"));
return false;
}
string name = UTF82String(filesystem::path(str2wstr(packPath)).filename().u8string());
addonLogger.warn(tr("ll.addonsHelper.install.installing", name));
std::error_code ec;
if (EndsWith(packPath, ".mcpack")) {
string newPath = packPath;
ReplaceStr(newPath, ".mcpack", ".zip");
filesystem::rename(str2wstr(packPath), str2wstr(newPath), ec);
packPath = newPath;
}
if (EndsWith(packPath, ".mcaddon")) {
string newPath = packPath;
ReplaceStr(newPath, ".mcaddon", ".zip");
filesystem::rename(str2wstr(packPath), str2wstr(newPath), ec);
packPath = newPath;
}
name = UTF82String(filesystem::path(str2wstr(packPath)).filename().u8string());
// filesystem::remove_all(ADDON_INSTALL_TEMP_DIR + name + "/", ec); //?
// filesystem::create_directories(ADDON_INSTALL_TEMP_DIR + name + "/", ec);
auto res = NewProcessSync(fmt::format("{} x \"{}\" -o{} -aoa", ZIP_PROGRAM_PATH, packPath, "\"" ADDON_INSTALL_TEMP_DIR + name + "/\""), ADDON_INSTALL_MAX_WAIT);
if (res.first != 0) {
addonLogger.error(tr("ll.addonsHelper.install.error.failToUncompress.msg", name));
addonLogger.error(tr("ll.addonsHelper.install.error.failToUncompress.exitCode"), res.first);
addonLogger.error(tr("ll.addonsHelper.install.error.failToUncompress.programOutput"), res.second);
addonLogger.error(tr("ll.addonsHelper.error.installationAborted"));
filesystem::remove_all(ADDON_INSTALL_TEMP_DIR + name + "/", ec);
return false;
}
vector<string> paths;
FindManifest(paths, ADDON_INSTALL_TEMP_DIR + name + "/");
for (auto& dir : paths) {
string addonName = UTF82String(filesystem::path(str2wstr(dir)).filename().u8string());
if (addonName.empty() || addonName == "Temp")
addonName = UTF82String(filesystem::path(str2wstr(packPath)).stem().u8string());
if (!InstallAddonToLevel(dir, addonName))
throw std::exception("Error in Install Addon To Level ");
}
filesystem::remove_all(ADDON_INSTALL_TEMP_DIR + name + "/", ec);
filesystem::remove_all(str2wstr(packPath), ec);
return true;
} catch (const seh_exception& e) {
addonLogger.error("Uncaught SEH Exception Detected!");
addonLogger.error("In " __FUNCTION__);
addonLogger.error("Error: Code[{}] {}", e.code(), TextEncoding::toUTF8(e.what()));
} catch (const std::exception& e) {
addonLogger.error("Uncaught C++ Exception Detected!");
addonLogger.error("In " __FUNCTION__);
addonLogger.error("Error: Code[{}] {}", -1, TextEncoding::toUTF8(e.what()));
} catch (...) {
addonLogger.error("Uncaught Exception Detected!");
addonLogger.error("In " __FUNCTION__);
}
return false;
}
bool AddonsManager::disable(std::string nameOrUuid) {
try {
auto addon = findAddon(nameOrUuid, true);
if (!addon)
return false;
if (RemoveAddonFromList(*addon)) {
addon->enable = false;
return true;
}
} catch (...) {}
return false;
}
bool AddonsManager::enable(std::string nameOrUuid) {
try {
auto addon = findAddon(nameOrUuid, true);
if (!addon)
return false;
if (AddAddonToList(*addon)) {
addon->enable = true;
return true;
}
} catch (...) {}
return false;
}
bool AddonsManager::uninstall(std::string nameOrUuid) {
try {
auto addon = findAddon(nameOrUuid, true);
if (!addon) {
addonLogger.error(tr("ll.addonsHelper.error.addonNotFound"));
return false;
}
RemoveAddonFromList(*addon);
std::error_code ec;
filesystem::remove_all(str2wstr(addon->directory), ec);
for (auto i = addons.begin(); i != addons.end(); ++i)
if (i->uuid == addon->uuid) {
addons.erase(i);
break;
}
addonLogger.info(tr("ll.addonsHelper.uninstall.success", addon->getPrintName()));
} catch (...) {}
return false;
}
std::vector<Addon*> AddonsManager::getAllAddons() {
std::vector<Addon*> res;
for (auto& addon : addons)
res.push_back(&addon);
return res;
}
Addon* AddonsManager::findAddon(std::string nameOrUuid, bool fuzzy) {
Addon* possible = nullptr;
bool multiMatch = false;
for (auto& addon : addons) {
if (addon.uuid == nameOrUuid)
return &addon;
std::string addonName = addon.name;
std::string targetName = nameOrUuid;
if (ColorFormat::removeColorCode(addonName) == ColorFormat::removeColorCode(targetName))
return &addon;
if (!fuzzy)
continue;
// Simple fuzzy matching
std::transform(addonName.begin(), addonName.end(), addonName.begin(), ::tolower);
std::transform(targetName.begin(), targetName.end(), targetName.begin(), ::tolower);
if (StartsWith(addonName, targetName)) {
if (possible)
multiMatch = true;
else
possible = &addon;
}
}
if (multiMatch)
return nullptr;
else
return possible;
}
void ListAllAddons(CommandOutput& output) {
if (addons.empty()) {
output.trSuccess("ll.addonsHelper.error.noAddonInstalled");
return;
}
output.trSuccess("ll.addonsHelper.cmd.output.list.overview", addons.size());
for (auto index = 0; index < addons.size(); ++index) {
auto& addon = addons[index];
string addonName = addon.name;
if (addonName.find("§") == string::npos)
addonName = "§b" + addonName;
string desc = addon.description;
if (desc.find("§") == string::npos)
desc = "§7" + desc;
std::string addonType = (addon.type == Addon::Type::ResourcePack ? "ResourcePack" : "BehaviorPack");
if (addon.enable) {
output.success(fmt::format("§e{:>2}§r: {} §a[v{}] §8({})", index + 1, addonName, addon.version.toString(), addonType));
output.success(fmt::format(" {}", desc));
} else {
output.success(fmt::format("§e{:>2}§r: §8{} [v{}] ({})", index + 1, ColorFormat::removeColorCode(addonName), addon.version.toString(), addonType));
output.success(fmt::format(" §8Disabled"));
}
}
}
void ShowAddon(CommandOutput& output, Addon& addon) {
std::ostringstream oss;
oss << "Addon <" << addon.name << "§r>" << (addon.enable ? " §aEnabled" : " §cDisabled") << "\n\n";
oss << "- §aName§r: " << addon.name << "\n";
oss << "- §aUUID§r: " << addon.uuid << "\n";
oss << "- §aDescription§r: " << addon.description << "\n";
oss << "- §aVersion§r: v" << addon.version.toString(true) << "\n";
oss << "- §aType§r: " << magic_enum::enum_name(addon.type) << "\n";
oss << "- §aDirectory§r: " << addon.directory << "\n";
output.success(oss.str());
}
class AddonsCommand : public Command {
enum class Operation {
List,
Install,
Uninstall,
Enable,
Disable
};
Operation operation = static_cast<Operation>(-1);
std::string target;
int index = -1;
bool target_isSet = false;
bool index_isSet = false;
inline Addon* getSelectedAddon(CommandOutput& output) const {
Addon* addon = nullptr;
if (target_isSet) {
auto addon = AddonsManager::findAddon(target, true);
if (addon) {
return addon;
} else {
output.trError("ll.addonsHelper.error.addonNotfound", target);
}
} else {
auto addons = AddonsManager::getAllAddons();
if (index - 1 >= 0 && index - 1 < static_cast<int>(addons.size()))
return addons[index - 1];
else
output.trError("ll.addonsHelper.error.outOfRange", index);
}
return nullptr;
}
public:
void execute(CommandOrigin const& ori, CommandOutput& output) const override {
output.setLanguageCode(ori);
switch (operation) {
case Operation::List:
if (target_isSet || index_isSet) {
if (auto addon = getSelectedAddon(output))
ShowAddon(output, *addon);
} else
ListAllAddons(output);
break;
case Operation::Install:
if (AddonsManager::install(target))
filesystem::remove_all(ADDON_INSTALL_TEMP_DIR);
output.success();
break;
case Operation::Uninstall: {
auto addon = getSelectedAddon(output);
if (addon && AddonsManager::uninstall(addon->uuid))
output.success();
break;
}
case Operation::Enable: {
auto addon = getSelectedAddon(output);
if (addon && AddonsManager::enable(addon->uuid))
output.success();
break;
}
case Operation::Disable: {
auto addon = getSelectedAddon(output);
if (addon && AddonsManager::disable(addon->uuid))
output.success();
break;
}
default:
break;
}
}
static void setup(CommandRegistry* registry) {
registry->registerCommand("addons", "LiteLoaderBDS Addons Helper (Restart required after addon changes)",
CommandPermissionLevel::GameMasters, {(CommandFlagValue)0}, {(CommandFlagValue)0x80});
vector<string> addonsList;
for (auto& addon : addons)
addonsList.push_back(addon.name);
registry->addSoftEnum("AddonName", addonsList);
// addons list
registry->addEnum<Operation>("Operation_Addons_List", {{"list", Operation::List}});
registry->registerOverload<AddonsCommand>(
"addons",
makeMandatory<CommandParameterDataType::ENUM>(&AddonsCommand::operation, "operation", "Operation_Addons_List").addOptions((CommandParameterOption)1),
makeOptional<CommandParameterDataType::SOFT_ENUM>(&AddonsCommand::target, "addonName", "AddonName", &AddonsCommand::target_isSet));
registry->registerOverload<AddonsCommand>(
"addons",
makeMandatory<CommandParameterDataType::ENUM>(&AddonsCommand::operation, "operation", "Operation_Addons_List").addOptions((CommandParameterOption)1),
makeOptional<CommandParameterDataType::NORMAL>(&AddonsCommand::index, "addonIndex", nullptr, &AddonsCommand::index_isSet));
// addons install
registry->addEnum<Operation>("Operation_Addons_Install", {{"install", Operation::Install}});
registry->registerOverload<AddonsCommand>(
"addons",
makeMandatory<CommandParameterDataType::ENUM>(&AddonsCommand::operation, "operation", "Operation_Addons_Install").addOptions((CommandParameterOption)1),
makeMandatory<CommandParameterDataType::NORMAL>(&AddonsCommand::target, "addonName"));
// addons uninstall
registry->addEnum<Operation>("Operation_Addons_Others", {
{"uninstall", Operation::Uninstall},
{"remove", Operation::Uninstall},
{"enable", Operation::Enable},
{"disable", Operation::Disable},
});
registry->registerOverload<AddonsCommand>(
"addons",
makeMandatory<CommandParameterDataType::ENUM>(&AddonsCommand::operation, "operation", "Operation_Addons_Others").addOptions((CommandParameterOption)1),
makeMandatory<CommandParameterDataType::SOFT_ENUM>(&AddonsCommand::target, "addonName", "AddonName", &AddonsCommand::target_isSet));
registry->registerOverload<AddonsCommand>(
"addons",
makeMandatory<CommandParameterDataType::ENUM>(&AddonsCommand::operation, "operation", "Operation_Addons_Others").addOptions((CommandParameterOption)1),
makeMandatory<CommandParameterDataType::NORMAL>(&AddonsCommand::index, "addonIndex", nullptr, &AddonsCommand::index_isSet));
}
};
void FindAddons(string jsonPath, string packsDir) {
try {
if (!filesystem::exists(str2wstr(jsonPath)) && !filesystem::exists(str2wstr(packsDir)))
return;
if (!filesystem::exists(str2wstr(jsonPath)))
WriteAllFile(jsonPath, "[]");
if (!filesystem::exists(str2wstr(packsDir)))
filesystem::create_directories(str2wstr(packsDir));
auto content = ReadAllFile(jsonPath);
if (!content || content->empty()) {
WriteAllFile(jsonPath, "[]");
content = "[]";
}
std::set<string> validPackIDs;
try {
auto addonList = nlohmann::json::parse(*content, nullptr, true, true);
for (auto addon : addonList) {
std::string pktid = addon["pack_id"];
validPackIDs.insert(pktid);
}
} catch (const std::exception&) {
addonLogger.error(tr("ll.addonsHelper.error.parsingEnabledAddonsList"));
}
filesystem::directory_iterator ent(str2wstr(packsDir));
for (auto& dir : ent) {
if (!dir.is_directory())
continue;
auto addon = parseAddonFromPath(dir);
if (!addon)
continue;
if (validPackIDs.find(addon->uuid) != validPackIDs.end())
addon->enable = true;
addons.emplace_back(std::move(*addon));
}
} catch (...) {
return;
}
}
void BuildAddonsList() {
string levelPath = Level::getCurrentLevelPath();
FindAddons(levelPath + "/world_behavior_packs.json", levelPath + "/behavior_packs");
FindAddons(levelPath + "/world_resource_packs.json", levelPath + "/resource_packs");
std::sort(addons.begin(), addons.end(),
[](Addon const& _Left, Addon const& _Right) {
if (_Left.enable && !_Right.enable)
return true;
if (_Left.type == Addon::Type::ResourcePack && _Right.type == Addon::Type::BehaviorPack)
return true;
return false;
});
}
bool AutoInstallAddons(string path) {
std::error_code ec;
if (!filesystem::exists(str2wstr(path))) {
filesystem::create_directories(str2wstr(path), ec);
addonLogger.info(tr("ll.addonsHelper.autoInstall.tip.dirCreated", LL::globalConfig.addonsInstallPath));
return false;
}
std::vector<string> toInstallList;
filesystem::directory_iterator ent(str2wstr(path));
for (auto& file : ent) {
if (!file.is_regular_file())
continue;
if (VALID_ADDON_FILE_EXTENSION.count(UTF82String(file.path().extension().u8string())) > 0) {
toInstallList.push_back(UTF82String(file.path().lexically_normal().u8string()));
}
}
if (toInstallList.empty())
return false;
addonLogger.info(tr("ll.addonsHelper.autoInstall.working", toInstallList.size()));
int cnt = 0;
for (auto& path : toInstallList) {
addonLogger.debug("Installing \"{}\"...", path);
if (!AddonsManager::install(path)) {
// filesystem::remove_all(ADDON_INSTALL_TEMP_DIR, ec);
break;
} else {
++cnt;
addonLogger.info(tr("ll.addonsHelper.autoInstall.installed", path));
}
}
if (cnt == 0) {
addonLogger.error(tr("ll.addonsHelper.error.noAddonInstalled"));
} else {
addonLogger.info(tr("ll.addonsHelper.autoInstall.installedCount", cnt));
}
return true;
}
void InitAddonsHelper() {
if (LL::isDebugMode())
addonLogger.consoleLevel = addonLogger.debug.level;
filesystem::remove_all(ADDON_INSTALL_TEMP_DIR);
filesystem::create_directories(ADDON_INSTALL_TEMP_DIR);
AutoInstallAddons(LL::globalConfig.addonsInstallPath);
BuildAddonsList();
filesystem::remove_all(ADDON_INSTALL_TEMP_DIR);
Event::RegCmdEvent::subscribe([](Event::RegCmdEvent ev) { // Register commands
AddonsCommand::setup(ev.mCommandRegistry);
return true;
});
}