mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
Completed basic functionality development
This commit is contained in:
@@ -1,315 +1,234 @@
|
||||
const sha256 = require('sha256')
|
||||
const createShortcut = require('create-desktop-shortcuts');
|
||||
const fs = require('fs');
|
||||
const fsp = require('fs').promises;
|
||||
const path = require('path');
|
||||
const { showWaitPopup } = require('./popup');
|
||||
const { Console } = require('console');
|
||||
const homeDir = require('os').homedir();
|
||||
|
||||
var output = fs.createWriteStream('./manager.log', {flags: 'a'});
|
||||
var output = fs.createWriteStream('./manager.log', { flags: 'a' });
|
||||
var logger = new Console(output, output);
|
||||
const date = new Date();
|
||||
output.write(` ======================= New log starting at ${date.toString()} =======================\n`);
|
||||
|
||||
/** Conveniency function to asynchronously delete a single file, with error catching
|
||||
*
|
||||
* @param {String} filePath The path to the file to delete
|
||||
*/
|
||||
async function deleteFile(filePath) {
|
||||
logger.log(`Deleting ${filePath}`);
|
||||
var promise = new Promise((res, rej) => {
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.rm(filePath, (err) => {
|
||||
if (err) {
|
||||
logger.error(`Error removing ${filePath}: ${err}`)
|
||||
rej(err);
|
||||
}
|
||||
else {
|
||||
logger.log(`Removed ${filePath}`)
|
||||
res(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
res(true);
|
||||
}
|
||||
})
|
||||
return promise;
|
||||
if (await exists(filePath) && await fsp.rm(filePath))
|
||||
logger.log(`Removed ${filePath}`);
|
||||
else
|
||||
logger.log(`${filePath} does not exist, nothing to do`);
|
||||
}
|
||||
|
||||
/** Given a list of Olympus instances, it fixes/updates them by deleting the existing installation and the copying over the clean files
|
||||
/** Conveniency function to asynchronously check if a file or folder exists
|
||||
*
|
||||
* @param {*} location Path to the folder or file to check for existance
|
||||
* @returns true if file exists, false if it doesn't
|
||||
*/
|
||||
async function fixInstances(instances) {
|
||||
var promise = new Promise((res, rej) => {
|
||||
var instancePromises = instances.map((instance) => {
|
||||
var instancePromise = new Promise((instanceRes, instanceErr) => {
|
||||
logger.log(`Fixing Olympus in ${instance.folder}`)
|
||||
deleteMod(instance.folder, instance.name)
|
||||
.then(() => deleteHooks(instance.folder), (err) => { return Promise.reject(err); })
|
||||
.then(() => installMod(instance.folder, instance.name), (err) => { return Promise.reject(err); })
|
||||
.then(() => installHooks(instance.folder), (err) => { return Promise.reject(err); })
|
||||
.then(() => installShortCuts(instance.folder, instance.name), (err) => { return Promise.reject(err); })
|
||||
.then(() => instanceRes(true), (err) => { instanceErr(err) })
|
||||
})
|
||||
return instancePromise;
|
||||
});
|
||||
Promise.all(instancePromises).then(() => res(true), (err) => { rej(err) });
|
||||
})
|
||||
return promise;
|
||||
async function exists(location) {
|
||||
try {
|
||||
await fsp.stat(location);
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
return false
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Uninstalls a specific instance given its folder
|
||||
*
|
||||
*/
|
||||
async function uninstallInstance(folder, name) {
|
||||
logger.log(`Uninstalling Olympus from ${folder}`)
|
||||
showWaitPopup("Please wait while the Olympus installation is being removed.")
|
||||
var promise = new Promise((res, rej) => {
|
||||
deleteMod(folder, name)
|
||||
.then(() => deleteHooks(folder), (err) => { return Promise.reject(err); })
|
||||
.then(() => deleteJSON(folder), (err) => { return Promise.reject(err); })
|
||||
.then(() => deleteShortCuts(folder, name), (err) => { return Promise.reject(err); })
|
||||
.then(() => res(true), (err) => { rej(err) });
|
||||
})
|
||||
return promise;
|
||||
}
|
||||
|
||||
/** Installs the Hooks script
|
||||
/** Asynchronously installs the Hooks script
|
||||
*
|
||||
* @param {String} folder The base Saved Games folder where the hooks scripts should be installed
|
||||
*/
|
||||
async function installHooks(folder) {
|
||||
logger.log(`Installing hooks in ${folder}`)
|
||||
var promise = new Promise((res, rej) => {
|
||||
fs.cp(path.join("..", "scripts", "OlympusHook.lua"), path.join(folder, "Scripts", "Hooks", "OlympusHook.lua"), (err) => {
|
||||
if (err) {
|
||||
logger.log(`Error installing hooks in ${folder}: ${err}`)
|
||||
rej(err);
|
||||
}
|
||||
else {
|
||||
logger.log(`Hooks succesfully installed in ${folder}`)
|
||||
res(true);
|
||||
}
|
||||
});
|
||||
})
|
||||
return promise;
|
||||
await fsp.cp(path.join("..", "scripts", "OlympusHook.lua"), path.join(folder, "Scripts", "Hooks", "OlympusHook.lua"));
|
||||
logger.log(`Hooks succesfully installed in ${folder}`)
|
||||
}
|
||||
|
||||
/** Installs the Mod folder
|
||||
/** Asynchronously installs the Mod folder
|
||||
*
|
||||
* @param {String} folder The base Saved Games folder where the mod folder should be installed
|
||||
* @param {String} name The name of the current DCS Instance, used to create backups of user created files
|
||||
*/
|
||||
|
||||
async function installMod(folder, name) {
|
||||
logger.log(`Installing mod in ${folder}`)
|
||||
var promise = new Promise((res, rej) => {
|
||||
fs.cp(path.join("..", "mod"), path.join(folder, "Mods", "Services", "Olympus"), { recursive: true }, (err) => {
|
||||
if (err) {
|
||||
logger.log(`Error installing mod in ${folder}: ${err}`)
|
||||
rej(err);
|
||||
}
|
||||
else {
|
||||
logger.log(`Mod succesfully installed in ${folder}`)
|
||||
|
||||
/* Check if backup user-editable files exist. If true copy them over */
|
||||
try {
|
||||
logger.log(__dirname)
|
||||
logger.log(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "databases"));
|
||||
if (fs.existsSync(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "databases"))) {
|
||||
logger.log("Backup databases found, copying over");
|
||||
fs.cpSync(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "databases"), path.join(folder, "Mods", "Services", "Olympus", "databases"), {recursive: true});
|
||||
}
|
||||
await fsp.cp(path.join("..", "mod"), path.join(folder, "Mods", "Services", "Olympus"), { recursive: true });
|
||||
logger.log(`Mod succesfully installed in ${folder}`)
|
||||
|
||||
if (fs.existsSync(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "scripts", "mods.lua"))) {
|
||||
logger.log("Backup mods.lua found, copying over");
|
||||
fs.cpSync(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "scripts", "mods.lua"), path.join(folder, "Mods", "Services", "Olympus", "scripts", "mods.lua"));
|
||||
}
|
||||
} catch (err) {
|
||||
logger.log(`Error installing mod in ${folder}: ${err}`)
|
||||
rej(err);
|
||||
}
|
||||
/* Check if backup user-editable files exist. If true copy them over */
|
||||
logger.log(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "databases"));
|
||||
if (await exists(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "databases"))) {
|
||||
logger.log("Backup databases found, copying over");
|
||||
await fsp.cp(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "databases"), path.join(folder, "Mods", "Services", "Olympus", "databases"), { recursive: true });
|
||||
}
|
||||
|
||||
res(true);
|
||||
}
|
||||
});
|
||||
})
|
||||
return promise;
|
||||
if (exists(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "scripts", "mods.lua"))) {
|
||||
logger.log("Backup mods.lua found, copying over");
|
||||
fsp.cp(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "scripts", "mods.lua"), path.join(folder, "Mods", "Services", "Olympus", "scripts", "mods.lua"));
|
||||
}
|
||||
}
|
||||
|
||||
/** Installs the olympus.json file
|
||||
/** Asynchronously installs the olympus.json file
|
||||
*
|
||||
* @param {String} folder The base Saved Games folder where the config json should be installed
|
||||
*/
|
||||
async function installJSON(folder) {
|
||||
logger.log(`Installing config in ${folder}`)
|
||||
var promise = new Promise((res, rej) => {
|
||||
fs.cp(path.join("..", "olympus.json"), path.join(folder, "Config", "olympus.json"), (err) => {
|
||||
if (err) {
|
||||
logger.log(`Error installing config in ${folder}: ${err}`)
|
||||
rej(err);
|
||||
}
|
||||
else {
|
||||
logger.log(`Config succesfully installed in ${folder}`)
|
||||
res(true);
|
||||
}
|
||||
});
|
||||
})
|
||||
return promise;
|
||||
await fsp.cp(path.join("..", "olympus.json"), path.join(folder, "Config", "olympus.json"));
|
||||
logger.log(`Config succesfully installed in ${folder}`)
|
||||
}
|
||||
|
||||
/** Creates shortcuts both in the DCS Saved Games folder and on the desktop
|
||||
/** Asynchronously creates shortcuts both in the DCS Saved Games folder and on the desktop
|
||||
*
|
||||
* @param {String} folder The base Saved Games folder where the shortcuts should be installed
|
||||
* @param {String} name The name of the current DCS Instance, used to create the shortcut names
|
||||
*/
|
||||
async function installShortCuts(folder, name) {
|
||||
logger.log(`Installing shortcuts for Olympus in ${folder}`);
|
||||
var promise = new Promise((res, rej) => {
|
||||
var res1 = createShortcut({
|
||||
windows: {
|
||||
filePath: path.resolve(__dirname, '..', '..', 'client', 'client.vbs'),
|
||||
outputPath: folder,
|
||||
name: `DCS Olympus Client (${name})`,
|
||||
arguments: `"${path.join(folder, "Config", "olympus.json")}"`,
|
||||
icon: path.resolve(__dirname, '..', '..', 'img', 'olympus.ico'),
|
||||
workingDirectory: path.resolve(__dirname, '..', '..', 'client')
|
||||
}
|
||||
});
|
||||
|
||||
var res2 = createShortcut({
|
||||
windows: {
|
||||
filePath: path.resolve(__dirname, '..', '..', 'client', 'server.vbs'),
|
||||
outputPath: folder,
|
||||
name: `DCS Olympus Server (${name})`,
|
||||
arguments: `"${path.join(folder, "Config", "olympus.json")}"`,
|
||||
icon: path.resolve(__dirname, '..', '..', 'img', 'olympus_server.ico'),
|
||||
workingDirectory: path.resolve(__dirname, '..', '..', 'client')
|
||||
}
|
||||
});
|
||||
|
||||
var res3 = createShortcut({
|
||||
windows: {
|
||||
filePath: path.resolve(__dirname, '..', '..', 'client', 'client.vbs'),
|
||||
name: `DCS Olympus Client (${name})`,
|
||||
arguments: `"${path.join(folder, "Config", "olympus.json")}"`,
|
||||
icon: path.resolve(__dirname, '..', '..', 'img', 'olympus.ico'),
|
||||
workingDirectory: path.resolve(__dirname, '..', '..', 'client')
|
||||
}
|
||||
});
|
||||
|
||||
var res4 = createShortcut({
|
||||
windows: {
|
||||
filePath: path.resolve(__dirname, '..', '..', 'client', 'server.vbs'),
|
||||
name: `DCS Olympus Server (${name})`,
|
||||
arguments: `"${path.join(folder, "Config", "olympus.json")}"`,
|
||||
icon: path.resolve(__dirname, '..', '..', 'img', 'olympus_server.ico'),
|
||||
workingDirectory: path.resolve(__dirname, '..', '..', 'client')
|
||||
}
|
||||
});
|
||||
|
||||
if (res1 && res2 && res3 && res4) {
|
||||
res(true);
|
||||
} else {
|
||||
rej("An error occurred while creating the shortcuts")
|
||||
var res1 = createShortcut({
|
||||
windows: {
|
||||
filePath: path.resolve(__dirname, '..', '..', 'client', 'client.vbs'),
|
||||
outputPath: folder,
|
||||
name: `DCS Olympus Client (${name})`,
|
||||
arguments: `"${path.join(folder, "Config", "olympus.json")}"`,
|
||||
icon: path.resolve(__dirname, '..', '..', 'img', 'olympus.ico'),
|
||||
workingDirectory: path.resolve(__dirname, '..', '..', 'client')
|
||||
}
|
||||
});
|
||||
return promise;
|
||||
|
||||
var res2 = createShortcut({
|
||||
windows: {
|
||||
filePath: path.resolve(__dirname, '..', '..', 'client', 'server.vbs'),
|
||||
outputPath: folder,
|
||||
name: `DCS Olympus Server (${name})`,
|
||||
arguments: `"${path.join(folder, "Config", "olympus.json")}"`,
|
||||
icon: path.resolve(__dirname, '..', '..', 'img', 'olympus_server.ico'),
|
||||
workingDirectory: path.resolve(__dirname, '..', '..', 'client')
|
||||
}
|
||||
});
|
||||
|
||||
var res3 = createShortcut({
|
||||
windows: {
|
||||
filePath: path.resolve(__dirname, '..', '..', 'client', 'client.vbs'),
|
||||
name: `DCS Olympus Client (${name})`,
|
||||
arguments: `"${path.join(folder, "Config", "olympus.json")}"`,
|
||||
icon: path.resolve(__dirname, '..', '..', 'img', 'olympus.ico'),
|
||||
workingDirectory: path.resolve(__dirname, '..', '..', 'client')
|
||||
}
|
||||
});
|
||||
|
||||
var res4 = createShortcut({
|
||||
windows: {
|
||||
filePath: path.resolve(__dirname, '..', '..', 'client', 'server.vbs'),
|
||||
name: `DCS Olympus Server (${name})`,
|
||||
arguments: `"${path.join(folder, "Config", "olympus.json")}"`,
|
||||
icon: path.resolve(__dirname, '..', '..', 'img', 'olympus_server.ico'),
|
||||
workingDirectory: path.resolve(__dirname, '..', '..', 'client')
|
||||
}
|
||||
});
|
||||
|
||||
// TODO actually check if the shortcuts where created
|
||||
if (!res1 || !res2 || !res3 || !res4)
|
||||
throw "An error occurred while creating the shortcuts";
|
||||
}
|
||||
|
||||
/** Writes the configuration of an instance to the olympus.json file
|
||||
/** Asynchronously writes the configuration of an instance to the olympus.json file
|
||||
*
|
||||
* @param {String} folder The base Saved Games folder where Olympus should is installed
|
||||
* @param {DCSInstance} instance The DCSInstance of which we want to apply the configuration
|
||||
*/
|
||||
async function applyConfiguration(folder, instance) {
|
||||
logger.log(`Applying configuration to Olympus in ${folder}`);
|
||||
var promise = new Promise((res, rej) => {
|
||||
if (fs.existsSync(path.join(folder, "Config", "olympus.json"))) {
|
||||
var config = JSON.parse(fs.readFileSync(path.join(folder, "Config", "olympus.json")));
|
||||
|
||||
config["client"]["port"] = instance.clientPort;
|
||||
config["server"]["port"] = instance.backendPort;
|
||||
config["server"]["address"] = instance.backendAddress;
|
||||
config["authentication"]["gameMasterPassword"] = sha256(instance.gameMasterPassword);
|
||||
config["authentication"]["blueCommanderPassword"] = sha256(instance.blueCommanderPassword);
|
||||
config["authentication"]["redCommanderPassword"] = sha256(instance.redCommanderPassword);
|
||||
if (await exists(path.join(folder, "Config", "olympus.json"))) {
|
||||
var config = JSON.parse(await fsp.readFile(path.join(folder, "Config", "olympus.json")));
|
||||
|
||||
fs.writeFile(path.join(folder, "Config", "olympus.json"), JSON.stringify(config, null, 4), (err) => {
|
||||
if (err) {
|
||||
logger.log(`Error applying config in ${folder}: ${err}`)
|
||||
rej(err);
|
||||
}
|
||||
else {
|
||||
logger.log(`Config succesfully applied in ${folder}`)
|
||||
res(true);
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
rej("File does not exist")
|
||||
/* Automatically find free ports */
|
||||
if (instance.connectionsType === 'auto') {
|
||||
await instance.findFreePorts();
|
||||
}
|
||||
res(true);
|
||||
});
|
||||
return promise;
|
||||
|
||||
/* Apply the configuration */
|
||||
config["client"]["port"] = instance.clientPort;
|
||||
config["server"]["port"] = instance.backendPort;
|
||||
config["server"]["address"] = instance.backendAddress;
|
||||
config["authentication"]["gameMasterPassword"] = sha256(instance.gameMasterPassword);
|
||||
config["authentication"]["blueCommanderPassword"] = sha256(instance.blueCommanderPassword);
|
||||
config["authentication"]["redCommanderPassword"] = sha256(instance.redCommanderPassword);
|
||||
|
||||
await fsp.writeFile(path.join(folder, "Config", "olympus.json"), JSON.stringify(config, null, 4));
|
||||
logger.log(`Config succesfully applied in ${folder}`)
|
||||
} else {
|
||||
throw "File does not exist";
|
||||
}
|
||||
}
|
||||
|
||||
/** Deletes the Hooks script
|
||||
/** Asynchronously deletes the Hooks script
|
||||
*
|
||||
* @param {String} folder The base Saved Games folder where Olympus is installed
|
||||
*/
|
||||
async function deleteHooks(folder) {
|
||||
logger.log(`Deleting hooks from ${folder}`);
|
||||
return deleteFile(path.join(folder, "Scripts", "Hooks", "OlympusHook.lua"));
|
||||
await deleteFile(path.join(folder, "Scripts", "Hooks", "OlympusHook.lua"));
|
||||
}
|
||||
|
||||
/** Deletes the Mod folder
|
||||
/** Asynchronously deletes the Mod folder
|
||||
*
|
||||
* @param {String} folder The base Saved Games folder where Olympus is installed
|
||||
*/
|
||||
async function deleteMod(folder, name) {
|
||||
logger.log(`Deleting mod from ${folder}`);
|
||||
var promise = new Promise((res, rej) => {
|
||||
if (fs.existsSync(path.join(folder, "Mods", "Services", "Olympus"))) {
|
||||
/* Make a copy of the user-editable files */
|
||||
if (fs.existsSync(path.join(folder, "Mods", "Services", "Olympus", "databases")))
|
||||
fs.cpSync(path.join(folder, "Mods", "Services", "Olympus", "databases"), path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "databases"), {recursive: true});
|
||||
else
|
||||
logger.warn(`No database folder found in ${folder}, skipping backup...`)
|
||||
|
||||
if (fs.existsSync(path.join(folder, "Mods", "Services", "Olympus", "scripts", "mods.lua")))
|
||||
fs.cpSync(path.join(folder, "Mods", "Services", "Olympus", "scripts", "mods.lua"), path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "scripts", "mods.lua"));
|
||||
else
|
||||
logger.warn(`No mods.lua found in ${folder}, skipping backup...`)
|
||||
if (await exists(path.join(folder, "Mods", "Services", "Olympus"))) {
|
||||
/* Make a copy of the user-editable files */
|
||||
if (await exists(path.join(folder, "Mods", "Services", "Olympus", "databases")))
|
||||
await fsp.cp(path.join(folder, "Mods", "Services", "Olympus", "databases"), path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "databases"), { recursive: true });
|
||||
else
|
||||
logger.warn(`No database folder found in ${folder}, skipping backup...`)
|
||||
|
||||
/* Remove the mod folder */
|
||||
fs.rmdir(path.join(folder, "Mods", "Services", "Olympus"), { recursive: true, force: true }, (err) => {
|
||||
if (err) {
|
||||
logger.log(`Error removing mod from ${folder}: ${err}`)
|
||||
rej(err);
|
||||
}
|
||||
else {
|
||||
logger.log(`Mod succesfully removed from ${folder}`)
|
||||
res(true);
|
||||
}
|
||||
})
|
||||
} else {
|
||||
res(true);
|
||||
};
|
||||
})
|
||||
return promise;
|
||||
if (await exists(path.join(folder, "Mods", "Services", "Olympus", "scripts", "mods.lua")))
|
||||
await fsp.cp(path.join(folder, "Mods", "Services", "Olympus", "scripts", "mods.lua"), path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "scripts", "mods.lua"));
|
||||
else
|
||||
logger.warn(`No mods.lua found in ${folder}, skipping backup...`)
|
||||
|
||||
/* Remove the mod folder */
|
||||
await fsp.rmdir(path.join(folder, "Mods", "Services", "Olympus"), { recursive: true, force: true })
|
||||
logger.log(`Mod succesfully removed from ${folder}`)
|
||||
} else {
|
||||
logger.warn(`Mod does not exist in ${folder}, nothing to do`)
|
||||
}
|
||||
}
|
||||
|
||||
/** Deletes the olympus.json configuration file
|
||||
/** Asynchronously deletes the olympus.json configuration file
|
||||
*
|
||||
* @param {String} folder The base Saved Games folder where Olympus is installed
|
||||
*/
|
||||
async function deleteJSON(folder) {
|
||||
logger.log(`Deleting JSON from ${folder}`);
|
||||
return deleteFile(path.join(folder, "Config", "olympus.json"));
|
||||
}
|
||||
|
||||
/** Deletes the shortcuts
|
||||
/** Asynchronously deletes the shortcuts
|
||||
*
|
||||
* @param {String} folder The base Saved Games folder where Olympus is installed
|
||||
* @param {String} name The name of the DCS Instance, used to find the correct shortcuts
|
||||
*/
|
||||
async function deleteShortCuts(folder, name) {
|
||||
logger.log(`Deleting ShortCuts from ${folder}`);
|
||||
var promise = new Promise((res, rej) => {
|
||||
deleteFile(path.join(folder, `DCS Olympus Server (${name}).lnk`))
|
||||
.then(deleteFile(path.join(folder, `DCS Olympus Client (${name}).lnk`)), (err) => { return Promise.reject(err); })
|
||||
.then(deleteFile(path.join(homeDir, "Desktop", `DCS Olympus Server (${name}).lnk`)), (err) => { return Promise.reject(err); })
|
||||
.then(deleteFile(path.join(homeDir, "Desktop", `DCS Olympus Client (${name}).lnk`)), (err) => { return Promise.reject(err); })
|
||||
.then(() => { res(true) }, (err) => { rej(err) })
|
||||
});
|
||||
return promise;
|
||||
logger.log(`Deleting ShortCuts from ${folder} and desktop`);
|
||||
await deleteFile(path.join(folder, `DCS Olympus Server (${name}).lnk`))
|
||||
await deleteFile(path.join(folder, `DCS Olympus Client (${name}).lnk`))
|
||||
await deleteFile(path.join(homeDir, "Desktop", `DCS Olympus Server (${name}).lnk`))
|
||||
await deleteFile(path.join(homeDir, "Desktop", `DCS Olympus Client (${name}).lnk`))
|
||||
logger.log(`ShortCuts deleted from ${folder} and desktop`);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
@@ -318,11 +237,9 @@ module.exports = {
|
||||
installHooks: installHooks,
|
||||
installMod: installMod,
|
||||
installShortCuts, installShortCuts,
|
||||
fixInstances: fixInstances,
|
||||
deleteHooks: deleteHooks,
|
||||
deleteJSON: deleteJSON,
|
||||
deleteMod: deleteMod,
|
||||
deleteShortCuts: deleteShortCuts,
|
||||
uninstallInstance: uninstallInstance,
|
||||
logger: logger
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user