aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--package-lock.json11
-rw-r--r--package.json3
-rw-r--r--src/app/index.html12
-rw-r--r--src/app/main.css48
-rw-r--r--src/app/main.js80
-rw-r--r--src/cli.js3
-rw-r--r--src/index.js35
-rw-r--r--src/lang/en.json21
-rw-r--r--src/utils.js181
9 files changed, 383 insertions, 11 deletions
diff --git a/package-lock.json b/package-lock.json
index 96d23eb..8bb3e29 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "0.9.0",
"license": "GPL-3.0-or-later",
"dependencies": {
+ "copy-dir": "^1.3.0",
"electron-updater": "^4.6.1",
"follow-redirects": "^1.14.6",
"marked-man": "^0.7.0",
@@ -1085,6 +1086,11 @@
"node": ">=8"
}
},
+ "node_modules/copy-dir": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/copy-dir/-/copy-dir-1.3.0.tgz",
+ "integrity": "sha512-Q4+qBFnN4bwGwvtXXzbp4P/4iNk0MaiGAzvQ8OiMtlLjkIKjmNN689uVzShSM0908q7GoFHXIPx4zi75ocoaHw=="
+ },
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@@ -4540,6 +4546,11 @@
"xdg-basedir": "^4.0.0"
}
},
+ "copy-dir": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/copy-dir/-/copy-dir-1.3.0.tgz",
+ "integrity": "sha512-Q4+qBFnN4bwGwvtXXzbp4P/4iNk0MaiGAzvQ8OiMtlLjkIKjmNN689uVzShSM0908q7GoFHXIPx4zi75ocoaHw=="
+ },
"core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
diff --git a/package.json b/package.json
index 449adb7..45eb7fc 100644
--- a/package.json
+++ b/package.json
@@ -29,11 +29,9 @@
"start": "npx electron src/index.js",
"debug": "npx electron src/index.js --debug",
"man": "npx marked-man docs/viper.1.md > docs/viper.1",
-
"build": "npx electron-builder --win nsis --win portable --linux",
"build:windows": "npx electron-builder --win nsis --win portable",
"build:linux": "npx electron-builder --linux",
-
"publish": "npx electron-builder --win nsis --win portable --linux -p always",
"publish:windows": "npx electron-builder --win nsis --win portable -p always",
"publish:linux": "npx electron-builder --linux -p always"
@@ -49,6 +47,7 @@
},
"homepage": "https://github.com/0neGal/viper#readme",
"dependencies": {
+ "copy-dir": "^1.3.0",
"electron-updater": "^4.6.1",
"follow-redirects": "^1.14.6",
"marked-man": "^0.7.0",
diff --git a/src/app/index.html b/src/app/index.html
index f5d92e0..88d5a17 100644
--- a/src/app/index.html
+++ b/src/app/index.html
@@ -26,6 +26,18 @@
<button id="northstar" onclick="launch()">%%gui.launchnorthstar%%</button>
</div>
</div>
+ <div id="modsdiv">
+ </div>
+ <div class="line">
+ <div class="text" id="modcount">%%gui.mods%%</div>
+ <div class="buttons modbtns">
+ <button id="removemod" onclick="selected().remove()">%%gui.mods.remove%%</button>
+ <button id="removeall" onclick="selected(true).remove()">%%gui.mods.removeall%%</button>
+ <button id="togglemod" onclick="selected().toggle()">%%gui.mods.toggle%%</button>
+ <button id="toggleall" onclick="selected(true).toggle(true)">%%gui.mods.toggleall%%</button>
+ <button id="installmod" onclick="installmod()">%%gui.mods.install%%</button>
+ </div>
+ </div>
</div>
<script src="lang.js"></script>
diff --git a/src/app/main.css b/src/app/main.css
index c2ca627..56f17b2 100644
--- a/src/app/main.css
+++ b/src/app/main.css
@@ -1,15 +1,22 @@
:root {
+ --padding: 15px;
--disabled: #656E7F;
- --background: #4C515B;
--foreground: #DDE2EB;
+ --background: #4C515B;
+ --boxbackground: #666E7F;
--subforeground: #AFAFAF;
--btnforeground: var(--foreground);
+
+ --red: #C7777F;
+ --blue: #81A1C1;
+ --yellow: #ECD19A;
}
@media (prefers-color-scheme: light) {
:root {
--background: #FFFFFF;
--foreground: #4C566A;
+ --boxbackground: #EEF0F4;
--btnforeground: var(--background);
}
}
@@ -19,6 +26,8 @@
src: url("roboto.ttf");
}
+::-webkit-scrollbar {display: none}
+
body, button, input {
font-size: 18px;
font-weight: 700;
@@ -40,34 +49,56 @@ nobr {white-space: nowrap}
.line {
display: flex;
- margin-top: 15px;
+ margin-top: var(--padding);
+}
+
+#modsdiv {
+ padding: 1px;
+ height: 125px;
+ overflow-y: scroll;
+ border-radius: var(--padding);
+ background: var(--boxbackground);
+ margin: calc(var(--padding) / 3) var(--padding);
+}
+
+.mod {
+ margin: calc(var(--padding) / 3);
+ border-radius: calc(var(--padding) / 2) !important;
+}
+
+.mod.selected {background: var(--background)}
+
+.mod .disabled {
+ color: var(--red);
+ position: relative;
+ left: var(--padding);
}
.buttons {
text-align: right;
margin-left: auto;
user-select: none;
- margin-right: 7px;
+ margin-right: calc(var(--padding) / 1.9);
}
.text {max-width: 38vw}
.buttons {max-width: 55vw}
-button, .text {
+button, .text, .mod {
border: none;
outline: none;
- padding: 5px 15px;
user-select: none;
border-radius: 50px;
transition: 0.2s ease-in-out;
+ padding: calc(var(--padding) / 3) var(--padding);
}
#welcome {padding: 0px}
button {
- margin-bottom: 10px;
color: var(--btnforeground);
-webkit-app-region: no-drag;
+ margin-bottom: calc(var(--padding) / 1.5);
}
button:hover {opacity: 0.9}
@@ -77,8 +108,9 @@ button:active {
}
-#update {background: #81A1C1}
#setpath {background: #5E81AC}
-#northstar {background: #C7777F}
#vanilla, #exit {background: #656E7F}
+#update, #installmod {background: var(--blue)}
+#togglemod, #toggleall {background: var(--yellow)}
+#northstar, #removeall, #removemod {background: var(--red)}
button:disabled {background: var(--disabled) !important; opacity: 0.5}
diff --git a/src/app/main.js b/src/app/main.js
index d30489e..6f2493a 100644
--- a/src/app/main.js
+++ b/src/app/main.js
@@ -45,6 +45,57 @@ function setButtons(state) {
}
}
+let lastselected = "";
+function select(entry) {
+ let entries = document.querySelectorAll("#modsdiv .mod .modtext");
+
+ for (let i = 0; i < entries.length; i++) {
+ if (entries[i].innerHTML == entry) {
+ lastselected = entry;
+ entries[i].parentElement.classList.add("selected");
+ } else {
+ entries[i].parentElement.classList.remove("selected");
+ }
+ }
+}
+
+function selected(all) {
+ let selected = "";
+ if (all) {
+ selected = "allmods"
+ } else {
+ selected = document.querySelector(".mod.selected .modtext");
+ if (selected != null) {
+ selected = selected.innerHTML;
+ } else {
+ alert(lang("gui.mods.nothingselected"));
+ return {
+ remove: () => {},
+ toggle: () => {},
+ }
+ }
+ }
+
+ return {
+ remove: () => {
+ if (selected == "allmods") {
+ if (! confirm(lang("gui.mods.removeall.confirm"))) {
+ return;
+ }
+ }
+
+ ipcRenderer.send("removemod", selected)
+ },
+ toggle: () => {
+ ipcRenderer.send("togglemod", selected)
+ }
+ }
+}
+
+function installmod() {
+ ipcRenderer.send("installmod")
+}
+
ipcRenderer.on("ns-updated", () => {setButtons(true)})
ipcRenderer.on("ns-updating", () => {setButtons(false)})
@@ -53,10 +104,39 @@ ipcRenderer.on("newpath", (event, newpath) => {
})
ipcRenderer.on("log", (event, msg) => {log(msg)})
+ipcRenderer.on("alert", (event, msg) => {alert(msg)})
+
+ipcRenderer.on("mods", (event, mods) => {
+ modcount.innerHTML = `${lang("gui.mods.count")} ${mods.all.length}`;
+ modsdiv.innerHTML = "";
+
+ let newmod = (name, disabled) => {
+ if (disabled) {
+ disabled = `<span class="disabled">${lang("gui.mods.disabledtag")}</span>`
+ } else {
+ disabled = ""
+ }
+
+ modsdiv.innerHTML += `<div onclick="select('${name}')" class="mod"><span class="modtext">${name}</span>${disabled}</div>`;
+ }
+
+ for (let i = 0; i < mods.enabled.length; i++) {newmod(mods.enabled[i].Name)}
+ for (let i = 0; i < mods.disabled.length; i++) {newmod(mods.disabled[i].Name, " - Disabled")}
+
+ select(lastselected);
+})
ipcRenderer.on("version", (event, versions) => {
vpversion.innerText = lang("gui.versions.viper") + ": " + versions.vp;
nsversion.innerText = lang("gui.versions.northstar") + ": " + versions.ns;
+
+ if (versions.ns == "unknown") {
+ let buttons = document.querySelectorAll(".modbtns button");
+
+ for (let i = 0; i < buttons.length; i++) {
+ buttons[i].disabled = true;
+ }
+ }
}); ipcRenderer.send("getversion");
ipcRenderer.on("updateavailable", () => {
diff --git a/src/cli.js b/src/cli.js
index b6594a5..55cee0e 100644
--- a/src/cli.js
+++ b/src/cli.js
@@ -10,6 +10,7 @@ const lang = require("./lang");
function hasArgs() {
if (cli.hasSwitch("cli") ||
cli.hasSwitch("help") ||
+ cli.hasSwitch("mods") ||
cli.hasSwitch("update") ||
cli.hasSwitch("launch") ||
cli.hasSwitch("setpath") ||
@@ -62,6 +63,8 @@ async function init() {
break;
}
}
+
+ if (cli.hasSwitch("mods")) {ipcMain.emit("getmods")}
}
module.exports = {
diff --git a/src/index.js b/src/index.js
index b43611d..a723622 100644
--- a/src/index.js
+++ b/src/index.js
@@ -42,6 +42,12 @@ function start() {
ipcMain.on("ns-updated", () => {win.webContents.send("ns-updated")})
ipcMain.on("ns-updating", () => {win.webContents.send("ns-updating")})
ipcMain.on("winLog", (event, ...args) => {win.webContents.send("log", ...args)})
+ ipcMain.on("winAlert", (event, ...args) => {win.webContents.send("alert", ...args)})
+ ipcMain.on("guigetmods", (event, ...args) => {win.webContents.send("mods", utils.mods.list())})
+
+ win.webContents.once("dom-ready", () => {
+ win.webContents.send("mods", utils.mods.list());
+ });
if (utils.settings.autoupdate) {utils.updatevp(false)}
@@ -53,6 +59,13 @@ function start() {
autoUpdater.quitAndInstall();
})
+ ipcMain.on("removemod", (event, mod) => {utils.mods.remove(mod)})
+ ipcMain.on("togglemod", (event, mod) => {utils.mods.toggle(mod)})
+ ipcMain.on("installmod", () => {
+ dialog.showOpenDialog({properties: ["openFile"]}).then(res => {
+ utils.mods.install(res.filePaths[0]);
+ }).catch(err => {console.error(err)})
+ })
}
ipcMain.on("launch", (event) => {utils.launch()})
@@ -77,6 +90,28 @@ ipcMain.on("versioncli", () => {
cli.exit();
})
+ipcMain.on("getmods", (event) => {
+ let mods = utils.mods.list();
+ if (mods.all.length > 0) {
+ console.log(`${utils.lang("general.mods.installed")} ${mods.all.length}`)
+ console.log(`${utils.lang("general.mods.enabled")} ${mods.enabled.length}`)
+ for (let i = 0; i < mods.enabled.length; i++) {
+ console.log(` ${mods.enabled[i].Name} ${mods.enabled[i].Version}`)
+ }
+
+ if (mods.disabled.length > 0) {
+ console.log(`${utils.lang("general.mods.disabled")} ${mods.disabled.length}`)
+ for (let i = 0; i < mods.disabled.length; i++) {
+ console.log(` ${mods.disabled[i].Name} ${mods.disabled[i].Version}`)
+ }
+ }
+ cli.exit(0);
+ } else {
+ console.log("No mods installed");
+ cli.exit(0);
+ }
+})
+
process.chdir(app.getPath("appData"));
if (cli.hasArgs()) {
diff --git a/src/lang/en.json b/src/lang/en.json
index 83f23f3..401e98f 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -18,6 +18,9 @@
"cli.launch.linuxerror": "Launching the game is not currently supported on Linux",
+ "cli.mods.notamod": "Selected folder/file is not a mod",
+ "cli.mods.improperjson": "%s's mod.json has formatting errors",
+
"gui.welcome": "Welcome to Viper!",
"gui.versions.viper": "Viper version",
"gui.versions.northstar": "Northstar version",
@@ -25,6 +28,21 @@
"gui.update": "Update",
"gui.setpath": "Game Path",
+ "gui.mods": "Mods",
+ "gui.mods.count": "Mods Installed:",
+ "gui.mods.disabledtag": "Disabled",
+ "gui.mods.install": "Install Mod",
+ "gui.mods.toggle": "Toggle Mod",
+ "gui.mods.toggleall": "Toggle All",
+ "gui.mods.remove": "Remove Mod",
+ "gui.mods.removeall": "Remove All",
+ "gui.mods.nothingselected": "You've not selected a mod.",
+ "gui.mods.removeall.confirm": "Removing all mods will usually require you to reinstall Northstar, are you sure?",
+ "gui.mods.notamod": "Not a mod!",
+ "gui.mods.extracting": "Extracting mod...",
+ "gui.mods.installing": "Installing mod...",
+ "gui.mods.installedmod": "Installed mod!",
+
"gui.update.downloading": "Downloading...",
"gui.update.extracting": "Extracting update...",
"gui.update.finished": "Done! Ready to play!",
@@ -39,5 +57,8 @@
"general.launching": "Launching",
+ "general.mods.enabled": "Enabled mods:",
+ "general.mods.disabled": "Disabled mods:",
+ "general.mods.installed": "Installed mods:",
"general.missingpath": "Game path is not set!"
}
diff --git a/src/utils.js b/src/utils.js
index e378366..5206226 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -1,5 +1,6 @@
-const fs = require("fs");
const path = require("path");
+const fs = require("fs-extra");
+const copy = require("copy-dir");
const { app, dialog, ipcMain } = require("electron");
const Emitter = require("events");
@@ -123,6 +124,7 @@ function update() {
}
}
+ ipcMain.emit("guigetmods");
ipcMain.emit("ns-updated");
winLog(lang("gui.update.finished"));
console.log(lang("cli.update.finished"));
@@ -171,7 +173,184 @@ function winLog(msg) {
ipcMain.emit("winLog", msg, msg);
}
+function winAlert(msg) {
+ ipcMain.emit("winAlert", msg, msg);
+}
+
+let modpath = path.join(settings.gamepath, "R2Northstar/mods");
+const mods = {
+ list: () => {
+ let mods = [];
+ let disabled = [];
+
+ files = fs.readdirSync(modpath)
+ files.forEach((file) => {
+ if (fs.statSync(path.join(modpath, file)).isDirectory()) {
+ if (fs.existsSync(path.join(modpath, file, "mod.json"))) {
+ try {
+ mods.push({...require(path.join(modpath, file, "mod.json")), FolderName: file, Disabled: false})
+ }catch(err) {
+ console.log("error: " + lang("cli.mods.improperjson"), file)
+ mods.push({Name: file, FolderName: file, Version: "unknown", Disabled: false})
+ }
+ }
+ }
+ })
+
+ let disabledPath = path.join(modpath, "disabled")
+ files = fs.readdirSync(disabledPath)
+ files.forEach((file) => {
+ if (fs.statSync(path.join(disabledPath, file)).isDirectory()) {
+ if (fs.existsSync(path.join(disabledPath, file, "mod.json"))) {
+ try {
+ disabled.push({...require(path.join(disabledPath, file, "mod.json")), FolderName: file, Disabled: true})
+ }catch(err) {
+ console.log("error: " + lang("cli.mods.improperjson"), file)
+ disabled.push({Name: file, FolderName: file, Version: "unknown", Disabled: true})
+ }
+ }
+ }
+ })
+
+ return {
+ enabled: mods,
+ disabled: disabled,
+ all: [...mods, ...disabled]
+ };
+ },
+ get: (mod) => {
+ let list = mods.list().all;
+
+ for (let i = 0; i < list.length; i++) {
+ if (list[i].Name == mod) {
+ return list[i];
+ } else {continue}
+ }
+
+ return false;
+ },
+ install: (mod) => {
+ let notamod = () => {
+ winLog(lang("gui.mods.notamod"))
+ console.log("error: " + lang("cli.mods.notamod"))
+ cli.exit(1);
+ }
+
+ let installed = () => {
+ cli.exit();
+ winLog(lang("gui.mods.installedmod"))
+ ipcMain.emit("guigetmods");
+ }
+
+ if (fs.statSync(mod).isDirectory()) {
+ winLog(lang("gui.mods.installing"))
+ if (fs.existsSync(path.join(mod, "mod.json")) &&
+ fs.statSync(path.join(mod, "mod.json")).isFile()) {
+
+ copy.sync(mod, path.join(modpath, mod.replace(/^.*(\\|\/|\:)/, "")), {
+ mode: true,
+ cover: true,
+ utimes: true,
+ });
+
+ installed();
+ return true;
+ } else {
+ files = fs.readdirSync(mod);
+
+ for (let i = 0; i < files.length; i++) {
+ if (fs.statSync(path.join(mod, files[i])).isDirectory()) {
+ if (fs.existsSync(path.join(mod, files[i], "mod.json")) &&
+ fs.statSync(path.join(mod, files[i], "mod.json")).isFile()) {
+
+ if (mods.install(path.join(mod, files[i]))) {return true};
+ }
+ }
+ }
+
+ notamod();
+ return false;
+ }
+ } else {
+ winLog(lang("gui.mods.extracting"))
+ let cache = path.join(app.getPath("userData"), "Archives");
+ if (fs.existsSync(cache)) {
+ fs.rmSync(cache, {recursive: true});
+ fs.mkdirSync(cache);
+ } else {
+ fs.mkdirSync(cache);
+ }
+
+ try {
+ fs.createReadStream(mod).pipe(unzip.Extract({path: cache}))
+ .on("finish", () => {
+ if (mods.install(cache)) {
+ installed();
+ } else {notamod();return false}
+ });
+ }catch(err) {notamod();return false}
+ }
+ },
+ remove: (mod) => {
+ if (mod == "allmods") {
+ let modlist = mods.list().all;
+ for (let i = 0; i < modlist.length; i++) {
+ mods.remove(modlist[i].Name)
+ }
+ return
+ }
+
+ let disabled = path.join(modpath, "disabled");
+ if (! fs.existsSync(disabled)) {
+ fs.mkdirSync(disabled)
+ }
+
+ let modName = mods.get(mod).FolderName;
+ let modPath = path.join(modpath, modName);
+
+ if (mods.get(mod).Disabled) {
+ modPath = path.join(disabled, modName);
+ }
+
+ if (fs.statSync(modPath).isDirectory()) {
+ fs.rmSync(modPath, {recursive: true});
+ cli.exit();
+ ipcMain.emit("guigetmods");
+ } else {
+ cli.exit(1);
+ }
+ },
+ toggle: (mod) => {
+ if (mod == "allmods") {
+ let modlist = mods.list().all;
+ for (let i = 0; i < modlist.length; i++) {
+ mods.toggle(modlist[i].Name)
+ }
+ return
+ }
+
+ let disabled = path.join(modpath, "disabled");
+ if (! fs.existsSync(disabled)) {
+ fs.mkdirSync(disabled)
+ }
+
+ let modName = mods.get(mod).FolderName;
+ let modPath = path.join(modpath, modName);
+ let dest = path.join(disabled, modName);
+
+ if (mods.get(mod).Disabled) {
+ modPath = path.join(disabled, modName);
+ dest = path.join(modpath, modName);
+ }
+
+ fs.moveSync(modPath, dest)
+ ipcMain.emit("guigetmods");
+ }
+};
+
module.exports = {
+ mods,
+ lang,
winLog,
launch,
update,