aboutsummaryrefslogtreecommitdiff
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
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
-rw-r--r--src/app/js/browser.js39
-rw-r--r--src/app/js/mods.js83
-rw-r--r--src/app/main.js21
-rw-r--r--src/index.js9
-rw-r--r--src/lang/de.json2
-rw-r--r--src/lang/en.json2
-rw-r--r--src/lang/es.json2
-rw-r--r--src/lang/fr.json2
-rw-r--r--src/modules/mods.js170
-rw-r--r--src/modules/packages.js458
10 files changed, 687 insertions, 101 deletions
diff --git a/src/app/js/browser.js b/src/app/js/browser.js
index 206b80e..06d3dd9 100644
--- a/src/app/js/browser.js
+++ b/src/app/js/browser.js
@@ -111,11 +111,14 @@ var Browser = {
browser.classList.toggle("shown");
},
install: (package_obj, clear_queue = false) => {
- console.log(package_obj)
return installFromURL(
package_obj.download || package_obj.versions[0].download_url,
package_obj.dependencies || package_obj.versions[0].dependencies,
- clear_queue, package_obj.owner
+ clear_queue,
+
+ package_obj.author || package_obj.owner,
+ package_obj.name || package_obj.pkg.name,
+ package_obj.version || package_obj.versions[0].version_number
)
},
add_pkg_properties: () => {
@@ -133,9 +136,9 @@ var Browser = {
for (let ii = 0; ii < modsobj.all.length; ii++) {
let mod = modsobj.all[ii];
- if (normalize(mod.Name) === normalized) {
- local_name = mod.Name;
- local_version = version.format(mod.Version);
+ if (normalize(mod.name) === normalized) {
+ local_name = mod.name;
+ local_version = version.format(mod.version);
if (version.is_newer(remote_version, local_version)) {
has_update = true;
}
@@ -270,13 +273,13 @@ var Browser = {
setTimeout(() => {
for (let i = 0; i < modsobj.all.length; i++) {
- let modname = normalize(modsobj.all[i].Name);
- let modfolder = normalize(modsobj.all[i].FolderName);
+ let modname = normalize(modsobj.all[i].name);
+ let modfolder = normalize(modsobj.all[i].folder_name);
if (mod.includes(modname)) {
if (! make(modname)) {
- if (modsobj.all[i].ManifestName) {
- make(normalize(modsobj.all[i].ManifestName));
+ if (modsobj.all[i].manifest_name) {
+ make(normalize(modsobj.all[i].manifest_name));
}
}
}
@@ -386,7 +389,7 @@ function BrowserEl(properties) {
let normalized_mods = [];
for (let i = 0; i < modsobj.all; i++) {
- normalized_mods.push(normalize(mods_list[i].Name));
+ normalized_mods.push(normalize(mods_list[i].name));
}
if (properties.pkg.local_version) {
@@ -445,8 +448,8 @@ function add_recent_toast(name, timeout = 3000) {
ipcRenderer.on("removed-mod", (event, mod) => {
setButtons(true);
Browser.setbutton(mod.name, lang("gui.browser.install"));
- if (mod.manifestname) {
- Browser.setbutton(mod.manifestname, lang("gui.browser.install"));
+ if (mod.manifest_name) {
+ Browser.setbutton(mod.manifest_name, lang("gui.browser.install"));
}
})
@@ -463,7 +466,7 @@ ipcRenderer.on("failed-mod", (event, modname) => {
})
})
-ipcRenderer.on("duped-mod", (event, modname) => {
+ipcRenderer.on("legacy-duped-mod", (event, modname) => {
if (recent_toasts["duped" + modname]) {return}
add_recent_toast("duped" + modname);
@@ -490,28 +493,30 @@ ipcRenderer.on("installed-mod", (event, mod) => {
if (recent_toasts["installed" + mod.name]) {return}
add_recent_toast("installed" + mod.name);
+ let name = mod.fancy_name || mod.name;
+
setButtons(true);
- Browser.setbutton(mod.name, lang("gui.browser.reinstall"));
+ Browser.setbutton(name, lang("gui.browser.reinstall"));
if (mod.malformed) {
new Toast({
timeout: 8000,
scheme: "warning",
title: lang("gui.toast.title.malformed"),
- description: mod.name + " " + lang("gui.toast.desc.malformed")
+ description: name + " " + lang("gui.toast.desc.malformed")
})
}
new Toast({
scheme: "success",
title: lang("gui.toast.title.installed"),
- description: mod.name + " " + lang("gui.toast.desc.installed")
+ description: name + " " + lang("gui.toast.desc.installed")
})
if (installqueue.length != 0) {
installFromURL(
"https://thunderstore.io/package/download/" + installqueue[0].pkg,
- false, false, installqueue[0].author
+ false, false, installqueue[0].author, installqueue[0].package_name, installqueue[0].version
)
installqueue.shift();
diff --git a/src/app/js/mods.js b/src/app/js/mods.js
index d4e8043..0d516d9 100644
--- a/src/app/js/mods.js
+++ b/src/app/js/mods.js
@@ -6,13 +6,18 @@ mods.load = (mods_obj) => {
let normalized_names = [];
let set_mod = (mod) => {
- let normalized_name = "mod-list-" + normalize(mod.Name);
+ let name = mod.name;
+ if (mod.package) {
+ name = mod.package.package_name;
+ }
+
+ let normalized_name = "mod-list-" + normalize(name);
normalized_names.push(normalized_name);
let el = document.getElementById(normalized_name);
if (el) {
- if (mod.Disabled) {
+ if (mod.disabled) {
el.querySelector(".switch").classList.remove("on");
} else {
el.querySelector(".switch").classList.add("on");
@@ -25,36 +30,67 @@ mods.load = (mods_obj) => {
div.classList.add("el");
div.id = normalized_name;
+ let mod_details = {
+ name: mod.name,
+ version: mod.version,
+ description: mod.description
+ }
+
+ if (mod.package) {
+ mod_details = {
+ image: mod.package.icon,
+ name: mod.package.manifest.name,
+ version: mod.package.manifest.version_number,
+ description: mod.package.manifest.description
+ }
+ }
+
div.innerHTML += `
<div class="image">
- <img src="">
+ <img src="${mod_details.image || ""}">
<img class="blur" src="">
</div>
<div class="text">
- <div class="title">${mod.Name}</div>
- <div class="description">${mod.Description}</div>
+ <div class="title">${mod_details.name}</div>
+ <div class="description">${mod_details.description}</div>
<button class="switch on orange"></button>
<button class="update bg-blue">
${lang("gui.browser.update")}
</button>
- <button class="bg-red" onclick="mods.remove('${mod.Name}')">
+ <button class="bg-red remove">
${lang("gui.mods.remove")}
</button>
- <button class="visual">${version.format(mod.Version)}</button>
+ <button class="visual">${version.format(mod_details.version)}</button>
<button class="visual">
${lang("gui.browser.madeby")}
- ${mod.Author || lang("gui.mods.unknown_author")}
+ ${mod.author || lang("gui.mods.unknown_author")}
</button>
</div>
`;
- if (mod.Disabled) {
+ div.querySelector(".remove").onclick = () => {
+ if (! mod.package) {
+ return mods.remove(mod.name);
+ }
+
+ for (let i = 0; i < mod.packaged_mods.length; i++) {
+ mods.remove(mod.packaged_mods[i]);
+ }
+ }
+
+ if (mod.disabled) {
div.querySelector(".switch").classList.remove("on");
}
div.querySelector(".switch").addEventListener("click", () => {
- mods.toggle(mod.Name);
+ if (! mod.package) {
+ return mods.toggle(mod.name);
+ }
+
+ for (let i = 0; i < mod.packaged_mods.length; i++) {
+ mods.toggle(mod.packaged_mods[i]);
+ }
})
div.querySelector(".image").style.display = "none";
@@ -87,20 +123,23 @@ mods.load = (mods_obj) => {
return;
}
+ let image_container = mod_els[i].querySelector(".image");
+ let image_el = image_container.querySelector("img")
+ let image_blur_el = image_container.querySelector("img.blur")
+
if (mod_versions[mod]) {
- let image_url = mod_versions[mod].package.versions[0].icon;
+ image_el.src = mod_versions[mod].package.versions[0].icon;
+ }
- let image_container = mod_els[i].querySelector(".image");
- let image_el = image_container.querySelector("img")
- let image_blur_el = image_container.querySelector("img.blur")
+ if (image_el.getAttribute("src") &&
+ ! image_container.parentElement.classList.contains("has-icon")) {
- if (image_url && ! image_el.getAttribute("src")) {
- image_container.style.display = null;
- image_el.src = image_url;
- image_blur_el.src = image_url;
+ let image_src = image_el.getAttribute("src");
- image_container.parentElement.classList.add("has-icon");
- }
+ image_blur_el.src = image_src;
+ image_container.style.display = null;
+
+ image_container.parentElement.classList.add("has-icon");
}
if (mod_versions[mod]
@@ -118,6 +157,10 @@ mods.load = (mods_obj) => {
let mod_el = mod_els[i].cloneNode(true);
+ // copy click event of the remove button to the new button
+ mod_el.querySelector(".remove").onclick =
+ mod_els[i].querySelector(".remove").onclick;
+
mod_el.classList.add("no-animation");
mod_el.querySelector(".switch").addEventListener("click", () => {
diff --git a/src/app/main.js b/src/app/main.js
index 1dbff40..51d9e73 100644
--- a/src/app/main.js
+++ b/src/app/main.js
@@ -3,7 +3,12 @@ const path = require("path");
const { app, ipcRenderer, shell } = require("electron");
const lang = require("../lang");
-var modsobj = {};
+var modsobj = {
+ all: [],
+ enabled: [],
+ disabled: []
+}
+
let shouldInstallNorthstar = false;
// Base settings
@@ -168,7 +173,7 @@ function installFromPath(path) {
}
// Tells the main process to install a mod from a URL
-function installFromURL(url, dependencies, clearqueue, author) {
+function installFromURL(url, dependencies, clearqueue, author, package_name, version) {
if (clearqueue) {installqueue = []};
let prettydepends = [];
@@ -183,7 +188,9 @@ function installFromURL(url, dependencies, clearqueue, author) {
if (! isModInstalled(pkg[1])) {
newdepends.push({
pkg: depend,
- author: pkg[0]
+ author: pkg[0],
+ version: pkg[2],
+ package_name: pkg[1]
});
prettydepends.push(`${pkg[1]} v${pkg[2]} - ${lang("gui.browser.madeby")} ${pkg[0]}`);
@@ -202,7 +209,7 @@ function installFromURL(url, dependencies, clearqueue, author) {
}
setButtons(false);
- ipcRenderer.send("install-from-url", url, author);
+ ipcRenderer.send("install-from-url", url, author, package_name, version);
if (dependencies) {
installqueue = dependencies;
@@ -212,11 +219,11 @@ function installFromURL(url, dependencies, clearqueue, author) {
function isModInstalled(modname) {
for (let i = 0; i < modsobj.all.length; i++) {
let mod = modsobj.all[i];
- if (mod.ManifestName) {
- if (mod.ManifestName.match(modname)) {
+ if (mod.manifest_name) {
+ if (mod.manifest_name.match(modname)) {
return true;
}
- } else if (mod.Name.match(modname)) {
+ } else if (mod.name.match(modname)) {
return true;
}
}
diff --git a/src/index.js b/src/index.js
index 259f7cc..3e7e08f 100644
--- a/src/index.js
+++ b/src/index.js
@@ -19,6 +19,7 @@ const version = require("./modules/version");
const gamepath = require("./modules/gamepath");
const settings = require("./modules/settings");
const requests = require("./modules/requests");
+const packages = require("./modules/packages");
const is_running = require("./modules/is_running");
console = require("./modules/console");
@@ -112,7 +113,9 @@ function start() {
// install calls
ipcMain.on("install-from-path", (event, path) => {mods.install(path)});
- ipcMain.on("install-from-url", (event, url, author) => {mods.installFromURL(url, author)});
+ ipcMain.on("install-from-url", (event, url, author, package_name, version) => {
+ packages.install(url, author, package_name, version);
+ });
win.webContents.on("dom-ready", () => {
send("mods", mods.list());
@@ -238,13 +241,13 @@ ipcMain.on("getmods", () => {
log(`${lang("general.mods.installed")} ${mods.all.length}`);
log(`${lang("general.mods.enabled")} ${mods.enabled.length}`);
for (let i = 0; i < mods.enabled.length; i++) {
- log(` ${mods.enabled[i].Name} ${mods.enabled[i].Version}`);
+ log(` ${mods.enabled[i].name} ${mods.enabled[i].version}`);
}
if (mods.disabled.length > 0) {
log(`${lang("general.mods.disabled")} ${mods.disabled.length}`);
for (let i = 0; i < mods.disabled.length; i++) {
- log(` ${mods.disabled[i].Name} ${mods.disabled[i].Version}`);
+ log(` ${mods.disabled[i].name} ${mods.disabled[i].version}`);
}
}
cli.exit(0);
diff --git a/src/lang/de.json b/src/lang/de.json
index 744fbc0..6ee32ae 100644
--- a/src/lang/de.json
+++ b/src/lang/de.json
@@ -69,6 +69,8 @@
"gui.mods.installedmod": "Mod installiert!",
"gui.mods.dragdrop": "Drag and drop den Mod um ihn zu installieren!",
"gui.mods.confirmdependencies": "Dieser Mod benötigt weitere Mods, diese werden unter dieser Nachricht angezeigt. Beim drücken auf \"Ok\" stimmst du zu da diese Installiert werden.\n\n",
+ "gui.mods.confirm_plugins_title": "Das folgende Packet hat native plugins::",
+ "gui.mods.confirm_plugins_description": "Native plugins haben sehr viel mehr Rechte als reguläre Mods, da durch ist das nutzen dieser um einiges unsicherer denn es ist einfacher ihnen zuschaden! Bitte installieren sie nur native plugins von vertrauten Entwicklern oder ähnliches, falls dir bewusst ist was du machst kannst du diese Nachricht ignorieren.",
"gui.browser.info": "Info",
"gui.browser.view": "Anschauen",
diff --git a/src/lang/en.json b/src/lang/en.json
index e510557..b3823d7 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -69,6 +69,8 @@
"gui.mods.installedmod": "Installed mod!",
"gui.mods.dragdrop": "Drag and drop a mod to install",
"gui.mods.confirmdependencies": "This package has dependencies, shown below, clicking \"Ok\" will install the package and the dependencies.\n\n",
+ "gui.mods.confirm_plugins_title": "The following package has native plugins:",
+ "gui.mods.confirm_plugins_description": "Native plugins have far more system access than a regular mod, and because of this they're inherently less secure to have installed, as a malicious plugin could do far more harm this way. If this plugin is one from a trusted developer or similar or you know what you're doing, then you can disregard this message completely.",
"gui.browser.info": "Info",
"gui.browser.view": "View",
diff --git a/src/lang/es.json b/src/lang/es.json
index 9139629..4afb2e6 100644
--- a/src/lang/es.json
+++ b/src/lang/es.json
@@ -69,6 +69,8 @@
"gui.mods.confirmdependencies": "Este paquete tiene dependencias, se muestran abajo. Presionar \"Ok\" instalará el paquete y las dependencias.\n\n",
"gui.mods.remove": "Remover",
"gui.mods.unknown_author": "Desconocido",
+ "gui.mods.confirm_plugins_title": "El siguiente paquete tiene complementos nativos:",
+ "gui.mods.confirm_plugins_description": "Los complementos nativos tienen mucho más acceso al sistema que un mod regular y, por lo tanto, son inherentemente menos seguros de instalar, ya que un complemento malicioso podría causar mucho más daño de esta manera. Si este complemento es de un desarrollador confiable o similar o si sabe lo que está haciendo, entonces puede ignorar completamente este mensaje.",
"gui.browser.info": "Información",
"gui.browser.madeby": "hecho por",
diff --git a/src/lang/fr.json b/src/lang/fr.json
index e04e2a2..cd168b3 100644
--- a/src/lang/fr.json
+++ b/src/lang/fr.json
@@ -69,6 +69,8 @@
"gui.mods.installedmod": "Mod installé !",
"gui.mods.dragdrop": "Glissez/déposez un mod pour l'installer",
"gui.mods.confirmdependencies": "Ce mod a des dépendances (affichées ci-dessous), cliquer \"Ok\" les installera en même temps que le mod.\n\n",
+ "gui.mods.confirm_plugins_title": "Ce mod contient des plugins :",
+ "gui.mods.confirm_plugins_description": "Les plugins ont des accès à votre système, comparés aux mods classiques, et sont de fait plus dangereux à l'installation, comme pourrait l'être un plugin contenant un malware. Si ce plugin provient d'un tiers de confiance ou si vous savez ce que vous faites, ne tenez pas compte de ce message.",
"gui.browser.info": "Info",
"gui.browser.view": "Voir",
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;