aboutsummaryrefslogtreecommitdiff
path: root/src/modules
diff options
context:
space:
mode:
author0neGal <mail@0negal.com>2023-07-24 20:38:21 +0200
committerGitHub <noreply@github.com>2023-07-24 20:38:21 +0200
commitb18038892db6567acb56822d1d7a1fe35f1c225f (patch)
treeafeac6f0d804be5d4f812afcbadc63ff8d2854fa /src/modules
parent112610902caef019ea4af17d77079cd090d6b9b5 (diff)
parent55040f6808f4aef3cd7ba86a45290d03963c37bd (diff)
downloadViper-b18038892db6567acb56822d1d7a1fe35f1c225f.tar.gz
Viper-b18038892db6567acb56822d1d7a1fe35f1c225f.zip
Merge pull request #191 from 0neGal/packages-dir
feat: Support for the new packages folder
Diffstat (limited to 'src/modules')
-rw-r--r--src/modules/mods.js170
-rw-r--r--src/modules/packages.js458
2 files changed, 574 insertions, 54 deletions
diff --git a/src/modules/mods.js b/src/modules/mods.js
index fc2a682..3c469c3 100644
--- a/src/modules/mods.js
+++ b/src/modules/mods.js
@@ -55,57 +55,111 @@ mods.list = () => {
};
}
- let files = fs.readdirSync(mods.path);
- files.forEach((file) => {
- // return early if `file` isn't a folder
- if (! fs.statSync(path.join(mods.path, file)).isDirectory()) {
- return;
- }
+ let get_in_dir = (dir, package_obj) => {
+ let packaged_mods = [];
+ let files = fs.readdirSync(dir);
+
+ files.forEach((file) => {
+ // return early if `file` isn't a folder
+ if (! fs.statSync(path.join(dir, file)).isDirectory()) {
+ return;
+ }
+
+ let modjson = path.join(dir, file, "mod.json");
+
+ // return early if mod.json doesn't exist or isn't a file
+ if (! fs.existsSync(modjson) || ! fs.statSync(modjson).isFile()) {
+ return;
+ }
+
+ let mod = json(modjson);
+ if (! mod) {return}
+
+ let obj = {
+ author: mod.Author || false,
+ version: mod.Version || "unknown",
+ name: mod.Name || "unknown",
+ description: mod.Description || "",
- let modjson = path.join(mods.path, file, "mod.json");
-
- // return early if mod.json doesn't exist or isn't a file
- if (! fs.existsSync(modjson) || ! fs.statSync(modjson).isFile()) {
+ folder_name: file,
+ folder_path: path.join(dir, file),
+
+ package: package_obj || false
+ }
+
+ if (obj.package) {
+ packaged_mods.push(obj.name);
+ obj.author = obj.package.author;
+ }
+
+ obj.disabled = ! mods.modfile.get(obj.name);
+
+ // add manifest data from manifest.json, if it exists
+ let manifest_file = path.join(dir, file, "manifest.json");
+ if (fs.existsSync(manifest_file)) {
+ let manifest = json(manifest_file);
+ if (manifest != false) {
+ obj.manifest_name = manifest.name;
+ if (obj.version == "unknown") {
+ obj.version = manifest.version_number;
+ }
+ }
+ }
+
+ // add author data from author file, if it exists
+ let author_file = path.join(dir, file, "thunderstore_author.txt");
+ if (fs.existsSync(author_file)) {
+ obj.author = fs.readFileSync(author_file, "utf8");
+ }
+
+ // add mod to their respective disabled or enabled Array
+ if (obj.disabled) {
+ disabled.push(obj);
+ } else {
+ enabled.push(obj);
+ }
+ })
+
+ if (packaged_mods.length == 0) {
return;
}
- let mod = json(modjson);
- if (! mod) {return}
-
- let obj = {
- Author: false,
- Version: "unknown",
- Name: "unknown",
- FolderName: file,
- ...mod}
-
- obj.Disabled = ! mods.modfile.get(obj.Name);
-
- // add manifest data from manifest.json, if it exists
- let manifest_file = path.join(mods.path, file, "manifest.json");
- if (fs.existsSync(manifest_file)) {
- let manifest = json(manifest_file);
- if (manifest != false) {
- obj.ManifestName = manifest.name;
- if (obj.Version == "unknown") {
- obj.Version = manifest.version_number;
+ let add_packaged_mods = (mods_array) => {
+ for (let i = 0; i < mods_array.length; i++) {
+ if (mods_array[i].package.package_name !==
+ package_obj.package_name) {
+
+ continue;
}
+
+ mods_array[i].packaged_mods = packaged_mods;
}
- }
- // add author data from author file, if it exists
- let author_file = path.join(mods.path, file, "thunderstore_author.txt");
- if (fs.existsSync(author_file)) {
- obj.Author = fs.readFileSync(author_file, "utf8");
+ return mods_array;
}
- // add mod to their respective disabled or enabled Array
- if (obj.Disabled) {
- disabled.push(obj);
- } else {
- enabled.push(obj);
+ enabled = add_packaged_mods(enabled);
+ disbled = add_packaged_mods(disabled);
+ }
+
+ // get mods in `mods` folder
+ get_in_dir(mods.path);
+
+ // get mods in `packages` folder
+ let packages = require("./packages");
+ let package_list = require("./packages").list(packages.path, true);
+ for (let i in package_list) {
+ // make sure the package actually has mods
+ if (! package_list[i].has_mods) {
+ continue;
}
- })
+
+ // search the package's `mods` folder
+ get_in_dir(
+ path.join(package_list[i].package_path, "mods"),
+ package_list[i]
+ )
+ }
return {
enabled: enabled,
@@ -136,7 +190,7 @@ mods.get = (mod) => {
// search for mod in list
for (let i = 0; i < list.length; i++) {
- if (list[i].Name == mod) {
+ if (list[i].name == mod) {
// found mod, return data
return list[i];
} else {continue}
@@ -176,7 +230,7 @@ mods.modfile.gen = () => {
let list = mods.list().all; // get list of all mods
for (let i = 0; i < list.length; i++) {
// add every mod to the list
- names[list[i].Name] = true
+ names[list[i].name] = true
}
// write the actual file
@@ -516,35 +570,43 @@ mods.remove = (mod) => {
if (mod == "allmods") {
let modlist = mods.list().all;
for (let i = 0; i < modlist.length; i++) {
- mods.remove(modlist[i].Name);
+ mods.remove(modlist[i].name);
}
return
}
- let mod_name = mods.get(mod).FolderName;
+ let mod_data = mods.get(mod);
+ let mod_name = mod_data.folder_name;
+
if (! mod_name) {
console.error("error: " + lang("cli.mods.cantfind"));
cli.exit(1);
return;
}
- let path_to_mod = path.join(mods.path, mod_name);
+ let mod_path = mod_data.folder_path;
+
+ // if the mod comes from a package, we'll want to set `mod_path` to
+ // the package's folder, that way everything gets removed cleanly
+ if (mod_data.package) {
+ mod_path = mod_data.package.package_path;
+ }
- // return early if path_to_mod isn't a folder
- if (! fs.statSync(path_to_mod).isDirectory()) {
+ // return early if `mod_path` isn't a folder
+ if (! fs.statSync(mod_path).isDirectory()) {
return cli.exit(1);
}
- let manifestname = null;
+ let manifest_name = null;
// if the mod has a manifest.json we want to save it now so we can
// send it later when telling the renderer about the deleted mod
- if (fs.existsSync(path.join(path_to_mod, "manifest.json"))) {
- manifestname = require(path.join(path_to_mod, "manifest.json")).name;
+ if (fs.existsSync(path.join(mod_path, "manifest.json"))) {
+ manifest_name = json(path.join(mod_path, "manifest.json")).name;
}
// actually remove the mod itself
- fs.rmSync(path_to_mod, {recursive: true});
+ fs.rmSync(mod_path, {recursive: true});
console.ok(lang("cli.mods.removed"));
cli.exit();
@@ -555,7 +617,7 @@ mods.remove = (mod) => {
// relevant info for it to properly update everything graphically
ipcMain.emit("removed-mod", "", {
name: mod.replace(/^.*(\\|\/|\:)/, ""),
- manifestname: manifestname
+ manifest_name: manifest_name
});
}
@@ -580,7 +642,7 @@ mods.toggle = (mod, fork) => {
if (mod == "allmods") {
let modlist = mods.list().all; // get list of all mods
for (let i = 0; i < modlist.length; i++) { // run through list
- mods.toggle(modlist[i].Name, true); // enable mod
+ mods.toggle(modlist[i].name, true); // enable mod
}
console.ok(lang("cli.mods.toggledall"));
diff --git a/src/modules/packages.js b/src/modules/packages.js
new file mode 100644
index 0000000..c9fb1c4
--- /dev/null
+++ b/src/modules/packages.js
@@ -0,0 +1,458 @@
+const path = require("path");
+const fs = require("fs-extra");
+const unzip = require("unzipper");
+const { app, ipcMain } = require("electron");
+const https = require("follow-redirects").https;
+
+const json = require("./json");
+const win = require("./window");
+const settings = require("./settings");
+
+console = require("./console");
+
+var packages = {};
+
+function update_path() {
+ packages.path = path.join(settings.gamepath, "R2Northstar/packages");
+
+ // make sure the `packages` folder exists
+ if (fs.existsSync(packages.path)) {
+ // if it does, but it's a file, remove it
+ if (fs.lstatSync(packages.path).isFile()) {
+ fs.rmSync(packages.path);
+ } else {return}
+ }
+
+ // only create folder if the profile folder exists
+ if (fs.existsSync(path.dirname(packages.path))) {
+ // create the folder, in case it doesn't already exist
+ fs.mkdirSync(packages.path);
+ }
+
+}; update_path();
+
+packages.format_name = (author, package_name, version) => {
+ return author + "-" + package_name + "-" + version;
+}
+
+// splits the package name into it's individual parts
+packages.split_name = (name) => {
+ let split = name.split("-");
+
+ // make sure there are only 3 parts
+ if (split.length !== 3) {
+ return false;
+ }
+
+ // return parts
+ return {
+ author: split[0],
+ version: split[2],
+ package_name: split[1]
+ }
+}
+
+packages.list = (dir = packages.path, no_functions) => {
+ let files = fs.readdirSync(dir);
+ let package_list = {};
+
+ for (let i = 0; i < files.length; i++) {
+ let package_path = path.join(dir, files[i]);
+ let verification = packages.verify(package_path);
+
+ let split_name = packages.split_name(files[i]);
+
+ if (! split_name) {continue}
+
+ // make sure the package is actually package
+ switch(verification) {
+ case true:
+ case "has-plugins":
+ package_list[files[i]] = {
+ // adds `author`, `package_name` and `version`
+ ...split_name,
+
+ icon: false, // will be set later
+ package_path: package_path, // path to package
+
+ // this is whether or not the package has plugins
+ has_plugins: (verification == "has-plugins"),
+
+ // this will be set later on
+ has_mods: false,
+
+ // contents of `manifest.json` or `false` if it can
+ // be parsed correctly
+ manifest: json(
+ path.join(package_path, "manifest.json")
+ ),
+ }
+
+ // if the package has a `mods` folder, and it's not
+ // empty, then we can assume that the package does
+ // indeed have mods
+ let mods_dir = path.join(package_path, "mods");
+ if (fs.existsSync(mods_dir) &&
+ fs.lstatSync(mods_dir).isDirectory() &&
+ fs.readdirSync(mods_dir).length >= 1) {
+
+ package_list[files[i]].has_mods = true;
+ }
+
+ // add `.remove()` function, mostly just a shorthand,
+ // unless `no_functions` is `true`
+ if (! no_functions) {
+ package_list[files[i]].remove = () => {
+ return packages.remove(
+ split_name.author,
+ split_name.package_name,
+ split_name.version,
+ )
+ }
+ }
+
+ // set the `.icon` property
+ let icon_file = path.join(package_path, "icon.png");
+ if (fs.existsSync(icon_file) &&
+ fs.lstatSync(icon_file).isFile()) {
+
+ package_list[files[i]].icon = icon_file;
+ }
+ break;
+ }
+ }
+
+ return package_list;
+}
+
+packages.remove = (author, package_name, version) => {
+ // if `version` is not set, we'll search for a package with the same
+ // `author` and `package_name` and use the version from that,
+ // this'll be useful when updating, of course this assumes that
+ // nobody has two versions of the same package installed
+ //
+ // TODO: perhaps we should remove duplicate packages?
+ if (! version) {
+ // get list of packages
+ let list = packages.list();
+
+ // iterate through them
+ for (let i in list) {
+ // check for `author` and `package_name` being the same
+ if (list[i].author == author &&
+ list[i].package_name == package_name) {
+
+ // set `version` to the found package
+ version = list[i].version;
+ break;
+ }
+ }
+ }
+
+ let name = packages.format_name(author, package_name, version);
+ let package_path = path.join(packages.path, name);
+
+ // make sure the package even exists to begin with
+ if (! fs.existsSync(package_path)) {
+ return false;
+ }
+
+ fs.rmSync(package_path, {recursive: true});
+
+ // return the inverse of whether the package still exists, this'll
+ // be equivalent to whether or not the removal was successful
+ return !! fs.existsSync(package_path);
+}
+
+packages.install = async (url, author, package_name, version) => {
+ update_path();
+
+ let name = packages.format_name(author, package_name, version);
+
+ // removes zip's and folders
+ let cleanup = () => {
+ console.info("Cleaning up cache folder of mod:", name);
+ if (zip_path && fs.existsSync(zip_path)) {
+ fs.rm(zip_path, {recursive: true});
+ console.ok("Cleaned archive of mod:", name);
+ }
+
+ if (package_path && fs.existsSync(package_path)) {
+ fs.rm(zip_path, {recursive: true});
+ console.ok("Cleaned mod folder:", name);
+ }
+
+ console.ok("Cleaned up cache folder of mod:", name);
+ }
+
+ console.info("Downloading package:", name);
+ // download `url` to a temporary dir, and return the path to it
+ let zip_path = await packages.download(url, name);
+
+ console.info("Extracting package:", name);
+ // extract the zip file we downloaded before, and return the path of
+ // the folder that we extracted it to
+ let package_path = await packages.extract(zip_path, name);
+
+
+ console.info("Verifying package:", name);
+ let verification = packages.verify(package_path);
+
+ switch(verification) {
+ case true: break;
+ case "has-plugins":
+ // if the package has plugins, then we want to prompt the
+ // user, and make absolutely certain that they do want to
+ // install this package, as plugins have security concerns
+ let confirmation = await win.confirm(
+ `${lang("gui.mods.confirm_plugins_title")} ${name} \n\n` +
+ lang("gui.mods.confirm_plugins_description")
+ )
+
+ // check whether the user cancelled or confirmed the
+ // installation, and act accordingly
+ if (! confirmation) {
+ return console.ok("Cancelled package installation:", name);
+ }
+ break;
+ default:
+ ipcMain.emit("failed-mod", name);
+
+ // other unhandled error
+ console.error(
+ "Verification of package failed:", name,
+ ", reason:", verification
+ );
+
+ return cleanup();
+ }
+
+ console.ok("Verified package:", name);
+
+ console.info("Deleting older version(s), if it exists:", name);
+ // check and delete any mod with the name package details in the old
+ // `mods` folder, if there are any at all
+ let mods = require("./mods");
+ let mods_list = mods.list().all;
+ for (let i = 0; i < mods_list.length; i++) {
+ let mod = mods_list[i];
+
+ if (mod.manifest_name == package_name) {
+ mods.remove(mod.name);
+ continue;
+ }
+
+ // normalizes a string, i.e attempt to make two strings
+ // identical, that simply have slightly different formatting, as
+ // an example, these strings:
+ //
+ // "Mod_Name" and "Mod name"
+ //
+ // will just become:
+ //
+ // "modname"
+ let normalize = (string) => {
+ return string.toLowerCase()
+ .replaceAll("_", "")
+ .replaceAll(".", "")
+ .replaceAll(" ", "");
+ }
+
+ // check if the mod's name from it's `mod.json` file when
+ // normalized, is the same as the normalized name of the package
+ if (normalize(mod.name) == normalize(package_name)) {
+ mods.remove(mod.name);
+ continue;
+ }
+
+ // check if the name of the mod's folder when normalized, is the
+ // same as the normalized name of the package
+ if (normalize(mod.folder_name) == normalize(package_name)) {
+ mods.remove(mod.name);
+ continue;
+ }
+ }
+
+ // removes older version of package inside the `packages` folder
+ packages.remove(author, package_name);
+ packages.remove(author, package_name, version);
+
+ console.info("Moving package:", name);
+ let moved = packages.move(package_path);
+
+ if (! moved) {
+ ipcMain.emit("failed-mod", name);
+ console.error("Moving package failed:", name);
+
+ cleanup();
+
+ return false;
+ }
+
+ ipcMain.emit("installed-mod", "", {
+ name: name,
+ fancy_name: package_name
+ })
+
+ console.ok("Installed package:", name);
+ cleanup();
+
+ return true;
+}
+
+packages.download = async (url, name) => {
+ update_path();
+
+ return new Promise((resolve) => {
+ // download mod to a temporary location
+ https.get(url, (res) => {
+ let tmp = path.join(app.getPath("cache"), "vipertmp");
+
+ let zip_name = name || "package";
+ let zip_path = path.join(tmp, `${zip_name}.zip`);
+
+ // make sure the temporary folder exists
+ if (fs.existsSync(tmp)) {
+ // if it's not a folder, then delete it
+ if (! fs.statSync(tmp).isDirectory()) {
+ fs.rmSync(tmp);
+ }
+ } else {
+ // create the folder
+ fs.mkdirSync(tmp);
+
+ // if there's already a zip file at `zip_path`, then we
+ // simple remove it, otherwise problems will occur
+ if (fs.existsSync(zip_path)) {
+ fs.rmSync(zip_path);
+ }
+ }
+
+ // write out the file to the temporary location
+ let stream = fs.createWriteStream(zip_path);
+ res.pipe(stream);
+
+ stream.on("finish", () => {
+ stream.close();
+
+ // return the path of the downloaded zip file
+ resolve(zip_path);
+ })
+ })
+ })
+}
+
+packages.extract = async (zip_path, name) => {
+ // this is where everything from `zip_path` will be extracted
+ let extract_dir = path.join(path.dirname(zip_path), name);
+
+ // delete `extract_dir` if it does exist
+ if (fs.existsSync(extract_dir)) {
+ fs.rmSync(extract_dir, {recursive: true});
+ }
+
+ // make an empty folder at `extract_dir`
+ fs.mkdirSync(extract_dir);
+
+ return new Promise((resolve) => {
+ fs.createReadStream(zip_path).pipe(
+ unzip.Extract({
+ path: extract_dir
+ }
+ )).on("finish", () => {
+ setInterval(() => {
+ resolve(extract_dir);
+ }, 1000)
+ });
+ })
+}
+
+packages.verify = (package_path) => {
+ // make sure `package_path` is even exists
+ if (! fs.existsSync(package_path)) {
+ return "does-not-exist";
+ }
+
+ // make sure `package_path` is not a folder
+ if (fs.lstatSync(package_path).isFile()) {
+ return "is-file";
+ }
+
+ // make sure a manifest file exists, this is required for
+ // Thunderstore packages, and is therefore also assumed to be
+ // required here
+ let manifest = path.join(package_path, "manifest.json");
+ if (! fs.existsSync(manifest) ||
+ fs.lstatSync(manifest).isDirectory()) {
+
+ return "missing-manifest";
+ }
+
+ // check if there are any plugins in the package
+ let mods_path = path.join(package_path, "mods");
+ let plugins = path.join(package_path, "plugins");
+ if (fs.existsSync(plugins) && fs.lstatSync(plugins).isDirectory()) {
+ // package has plugins, the function calling `packages.verify()`
+ // will have to handle this at their own discretion
+ return "has-plugins";
+ } else if (! fs.existsSync(mods_path) || fs.lstatSync(mods_path).isFile()) {
+ // if there are no plugins, then we check if there are any mods,
+ // if not, then it means there are both no plugins and mods, so
+ // we signal that back
+ return "no-mods";
+ }
+
+ // make sure files in the `mods` folder actually are mods, and if
+ // none of them are, then we make sure to return back that are no
+ // mods installed
+ let found_mod = false;
+ let mods = fs.readdirSync(mods_path);
+ for (let i = 0; i < mods.length; i++) {
+ let mod_file = path.join(mods_path, mods[i], "mod.json");
+
+ // make sure mod.json exists, and is a file, otherwise, this
+ // is unlikely to be a mod folder
+ if (! fs.existsSync(mod_file)
+ || ! fs.statSync(mod_file).isFile()) {
+ continue;
+ }
+
+ // attempt to read the mod.json file, and if it succeeds, then
+ // this is likely to be a mod
+ let json_data = json(mod_file);
+ if (json_data) {
+ found_mod = true;
+ }
+ }
+
+ if (! found_mod) {return "no-mods"}
+
+ // all files exist, and everything is just fine
+ return true;
+}
+
+// moves `package_path` to the packages folder
+packages.move = (package_path) => {
+ update_path();
+
+ // make sure we're actually dealing with a real folder
+ if (! fs.existsSync(package_path) ||
+ ! fs.lstatSync(package_path).isDirectory()) {
+
+ return false;
+ }
+
+ // get path to the package's destination
+ let new_path = path.join(
+ packages.path, path.basename(package_path)
+ )
+
+ // attempt to move `package_path` to the packages folder
+ try {
+ fs.moveSync(package_path, new_path);
+ }catch(err) {return false}
+
+ return true;
+}
+
+module.exports = packages;