Merge pull request #250 from Hornwitser/code-cleanup

Code cleanup
This commit is contained in:
Hornwitser authored and GitHub committed 2019-10-28 20:24:50 +01:00
commit f163e6a39b
34 files changed
+595 -946

No files matched your search

+1
View File
@@ -7,4 +7,5 @@ before_install:
- npm install -g codecov
script:
- npm run-script ci-cover
- cat test/temp/integration/log.txt
- codecov -f coverage/lcov.info
+7
View File
@@ -24,6 +24,7 @@ Version 2.0.0
- Added plugins directory to the views path. This makes it possible for
plugins to render their own ejs views or pages in their own folders by
using paths of the format "pluginName/path/to/page-or-view".
- Added instanceName entry to the config passed to instance plugins.
- Replaced the per instance copy of the shared Factorio mods with
symlinks. On Windows hard links are used instead due to the
privileges requirements of symlinks.
@@ -34,6 +35,12 @@ Version 2.0.0
- The game port, RCON port and RCON password instance config entries are are
now null by default, indicating that a random one will be generated every
time the instance is started.
- Removed the broken client download command as this would download
pre-releases.
- Moved the item database and HTTP definitions for /api/place, /api/remove,
/api/inventory, /api/inventoryAsObject and the web interface view for the
storage page into the clusterioMod plugin. If you disable this plugin
then these things will not be available.
### Breaking Changes
+1 -1
View File
@@ -3,7 +3,7 @@ const cli = require("cli");
const config = require("./../config.json");
let options = cli.parse()
cli.setUsage(`node cluster_rcon.js /c game.player.force = "player"`)
cli.setUsage(`node cluster_rcon /c game.player.force = "player"`)
if(process.argv.length < 3){
cli.getUsage()
process.exit(1)
+2 -2
View File
@@ -9,7 +9,7 @@ WORKDIR factorioClusterio
RUN mkdir instances sharedMods
RUN cp config.json.dist config.json
RUN node client.js download
RUN node client manage shared mods add clusterio
LABEL maintainer "Sir3lit@gmail.com"
@@ -18,4 +18,4 @@ VOLUME /factorioClusterio/instances
VOLUME /factorioClusterio/sharedMods
VOLUME /factorioClusterio/sharedPlugins
CMD RCONPORT="$RCONPORT" FACTORIOPORT="$FACTORIOPORT" MODE="$MODE" node $MODE\.js start $INSTANCE
CMD RCONPORT="$RCONPORT" FACTORIOPORT="$FACTORIOPORT" MODE="$MODE" node $MODE start $INSTANCE
+14 -14
View File
@@ -112,7 +112,7 @@ Master and all slaves:
tar -xf factorio.tar.gz
npm install --only=production
cp config.json.dist config.json
node ./lib/npmPostinstall.js
node ./lib/npmPostinstall
downloads and installs nodejs, git and clusterio. To specify a version, change "latest" in the link to a version number like 0.14.21.
@@ -131,26 +131,26 @@ Pretty much all the blank fields should be filled in, except on the master where
**Master**
node master.js
node master
Or with pm2 (it's recommened to run it without pm2 first):
pm2 start master.js --name master
pm2 start master --name master
**Server Host**
To download the mod for all its non vanilla features and items, (optional, but very recommended)
node client.js manage shared mods add clusterio
node client manage shared mods add clusterio
To create a new instance (its own save, set of mods and config files)
node client.js start [instancename]
node client start [instancename]
To launch an instance with pm2
pm2 start --name [instancename] client.js -- start [instancename]
pm2 start --name [instancename] client -- start [instancename]
use `nano config.json` to change settings.
@@ -196,9 +196,9 @@ reboot when you are done, then proceed to the next steps. *reboots matter*
3. Follow the instructions in the bat file
3. Some of the instructions are outdated. If you get stuck somewhere, look at the Ubuntu section.
4. Run `node client.js manage shared mods add clusterio`
4. Run `node client manage shared mods add clusterio`
5. type `node master.js` to start the server
5. type `node master` to start the server
**Server Host**
@@ -210,7 +210,7 @@ reboot when you are done, then proceed to the next steps. *reboots matter*
3.5 Some of the instructions are outdated. If you get stuck somewhere, look at the Ubuntu section.
4. Type `node client.js start [instancename]` to create a new instance.
4. Type `node client start [instancename]` to create a new instance.
To connect to a master server running on a remote machine, open config.json with your favourite text editor (notepad++). You can also set it up to use the official server browser.
@@ -268,19 +268,19 @@ TLDR: the tested fix is:
### Other fixes for other potential problems:
Sometimes the install fails. Try `node ./lib/npmPostinstall.js` to complete it.
Sometimes the install fails. Try `node ./lib/npmPostinstall` to complete it.
## Cheatsheet
**To create a new instance/start it**
node client.js start [instanceName]
node client start [instanceName]
**Other instance management tools:**
```
node client.js delete [instanceName]
node client.js list
node client delete [instanceName]
node client list
```
**To update clusterio to the latest version:**
@@ -296,5 +296,5 @@ npm install --only=production
3. Download the latest version of the factorioClusterioMod from its github repo
```
node client.js manage shared mods add clusterio
node client manage shared mods add clusterio
```
+14 -37
View File
@@ -1,10 +1,8 @@
const fs = require('fs-extra');
const Tail = require('tail').Tail;
const https = require('follow-redirects').https;
const needle = require("needle");
const child_process = require('child_process');
const path = require('path');
const syncRequest = require('sync-request');
const request = require("request");
const deepmerge = require("deepmerge");
const getMac = require('getmac').getMac;
@@ -13,7 +11,7 @@ const asTable = require("as-table").configure({delimiter: ' | '});
const util = require("util");
// internal libraries
const objectOps = require("lib/objectOps.js");
const objectOps = require("lib/objectOps");
const fileOps = require("lib/fileOps");
const pluginManager = require("lib/manager/pluginManager");
const modManager = require("lib/manager/modManager");
@@ -105,15 +103,15 @@ function needleOptionsWithTokenAuthHeader(config) {
function printUsage() {
console.error("Usage: ");
console.error("node client.js start [instance name]");
console.error("node client.js list");
console.error("node client.js delete [instance]");
console.error("node client start [instance name]");
console.error("node client list");
console.error("node client delete [instance]");
console.error("To download the latest version of the Clusterio lua mod, do");
console.error("node client.js manage shared mods download clusterio");
console.error("node client manage shared mods download clusterio");
console.error("To download a clusterio plugin, do");
console.error("node client.js manage shared plugins install https://github.com/Danielv123/playerManager");
console.error("node client manage shared plugins install https://github.com/Danielv123/playerManager");
console.error("For more management options, do");
console.error("node client.js manage");
console.error("node client manage");
}
async function listInstances(config) {
@@ -133,14 +131,14 @@ async function listInstances(config) {
async function manage(config, instance) {
// console.log("Launching mod manager");
//const fullUsage = 'node client.js manage [instance, "shared"] ["mods", "config"] ...';
//const fullUsage = 'node client manage [instance, "shared"] ["mods", "config"] ...';
function usage(instance, tool, action){
if(tool && tool == "mods"){
console.log('node client.js manage '+instance.name+' '+tool+' ["list", "search", "add", "remove"]');
console.log('node client manage '+instance.name+' '+tool+' ["list", "search", "add", "remove"]');
} else if(tool && tool == "plugins") {
console.log(`node client.js manage ${instance.name} ${tool} ["list", "add", "remove"]`);
console.log(`node client manage ${instance.name} ${tool} ["list", "add", "remove"]`);
} else {
console.log('node client.js manage '+(instance && instance.name || '[instance, "shared"]') +' '+ (tool || '["mods", "plugins"]') + ' ...');
console.log('node client manage '+(instance && instance.name || '[instance, "shared"]') +' '+ (tool || '["mods", "plugins"]') + ' ...');
}
}
const tool = process.argv[4] || "";
@@ -197,7 +195,7 @@ async function manage(config, instance) {
async function deleteInstance(instance) {
if (instance === undefined) {
console.error("Usage: node client.js delete [instance]");
console.error("Usage: node client delete [instance]");
process.exit(1);
} else if (fs.existsSync(instance.path())) {
fileOps.deleteFolderRecursiveSync(instance.path()); // TODO: Check if this can cause i-craft users to format their server by using wrong paths
@@ -209,25 +207,6 @@ async function deleteInstance(instance) {
}
}
async function downloadMod() {
console.log("Downloading mods...");
// get JSON data about releases
let res = syncRequest('GET', 'https://api.github.com/repos/Danielv123/factorioClusterioMod/releases', {"headers":{"User-Agent":"factorioClusterio"}});
let url = JSON.parse(res.getBody())[0].assets[0].browser_download_url;
let name = JSON.parse(res.getBody())[0].assets[0].name;
if(url) {
console.log(url);
let file = fs.createWriteStream("sharedMods/"+name);
https.get(url, function(response) {
response.on('end', function () {
console.log("Downloaded "+name);
process.exit(0);
});
response.pipe(file);
}).end();
}
}
/**
* Create and update symlinks for shared mods in an instance
*
@@ -542,11 +521,9 @@ async function startClient() {
// process.exit(0);
} else if (command == "delete") {
await deleteInstance(instance);
} else if (command == "download") {
await downloadMod();
} else if (command == "start" && instance === undefined) {
console.error("ERROR: No instanceName provided!");
console.error("Usage: node client.js start [instanceName]");
console.error("Usage: node client start [instanceName]");
process.exit(0);
} else if (command == "start" && !fs.existsSync(instance.path())) {
await createInstance(config, args, instance);
@@ -594,7 +571,7 @@ async function instanceManagement(config, instance, instanceconfig, server) {
}
if(pluginConfig.binary == "nodePackage" && pluginConfig.enabled){
// require plugin class and execute it
let pluginClass = require(path.resolve(pluginConfig.pluginPath, "index.js"));
let pluginClass = require(path.resolve(pluginConfig.pluginPath, "index"));
plugins[i] = new pluginClass(combinedConfig, async function(data, callback){
if(data && data.toString('utf8')[0] != "/") {
console.log("Clusterio | "+ pluginsToLoad[i].name + " | " + data.toString('utf8'));
+1 -1
View File
@@ -12,5 +12,5 @@ module.exports = ({config} = {}) => ({
arg1 is the instanceID
Promise resolves with a string, either the displayName or instanceID if not found.
*/
getInstanceName: require("./getInstanceName.js")(config),
getInstanceName: require("./getInstanceName")(config),
})
+1 -1
View File
@@ -1 +1 @@
module.exports = require("./isFactorioCommand.js");
module.exports = require("./isFactorioCommand");
+1 -1
View File
@@ -36,7 +36,7 @@ modManager.listMods(config, instance);
*/
async function listMods(config, instance) {
return new Promise((resolve, reject) => {
const instances = require("./../getInstances.js")(config).sync();
const instances = require("./../getInstances")(config).sync();
if (instances.includes(instance.name)) {
factorioAPI.init(false, instance.path("instanceMods"), instance.path("saves"), config.factorio_version);
factorioAPI.authenticate({
+2 -3
View File
@@ -1,12 +1,11 @@
var mkdirp = require("mkdirp");
var http = require('http');
var https = require('https');
var fs = require('fs');
var fs = require('fs-extra');
function makeDirectory(path){
return new Promise((resolve, reject) => {
mkdirp(path, function(err){
fs.mkdirp(path, function(err){
if(err){
console.error("Unable to mkdirp "+path);
console.error(err);
+22 -272
View File
@@ -7,17 +7,16 @@ combinator signals.
@author Danielv123
@example
node master.js
node master
*/
// Attempt updating
// const updater = require("./updater.js");
// const updater = require("./updater");
// updater.update().then(console.log);
// argument parsing
const args = require('minimist')(process.argv.slice(2));
const averagedTimeSeries = require("averaged-timeseries");
const deepmerge = require("deepmerge");
const path = require("path");
const fs = require("fs-extra");
@@ -32,7 +31,7 @@ let config = {};
// homebrew modules
const generateSSLcert = require("lib/generateSSLcert");
const pluginManager = require("lib/manager/pluginManager.js");
const pluginManager = require("lib/manager/pluginManager");
const database = require("lib/database");
const factorio = require("lib/factorio");
@@ -71,8 +70,8 @@ app.use(function(req, res, next){
next();
});
require("./routes.js")(app);
require("./routes/api/getPictures.js")(app);
require("./routes")(app);
require("./routes/api/getPictures")(app);
// Set folder to serve static content from (the website)
app.use(express.static('static'));
@@ -99,21 +98,6 @@ const prometheusWsUsageCounter = new Prometheus.Counter({
help: 'Websocket traffic',
labelNames: ["connectionType", "instanceID"],
});
const prometheusExportGauge = new Prometheus.Gauge({
name: prometheusPrefix+'export_gauge',
help: 'Items exported by instance',
labelNames: ["instanceID", "itemName"],
});
const prometheusImportGauge = new Prometheus.Gauge({
name: prometheusPrefix+'import_gauge',
help: 'Items imported by instance',
labelNames: ["instanceID", "itemName"],
});
const prometheusDoleFactorGauge = new Prometheus.Gauge({
name: prometheusPrefix+'dole_factor_gauge',
help: 'The current dole division factor for this item',
labelNames: ["itemName"],
});
const prometheusPlayerCountGauge = new Prometheus.Gauge({
name: prometheusPrefix+'player_count_gauge',
help: 'Amount of players connected to this cluster',
@@ -124,11 +108,6 @@ const prometheusUPSGauge = new Prometheus.Gauge({
help: 'UPS of the current server',
labelNames: ["instanceID", "instanceName"],
});
const prometheusMasterInventoryGauge = new Prometheus.Gauge({
name: prometheusPrefix+'master_inventory_gauge',
help: 'Amount of items stored on master',
labelNames: ["itemName"],
});
/**
GET Prometheus metrics endpoint. Returns performance and usage metrics in a prometheus readable format.
@memberof clusterioMaster
@@ -156,10 +135,14 @@ function registerMoreMetrics(){
if(slave.meta.tick && typeof slave.meta.tick === "number") prometheusUPSGauge.labels(instanceID, slave.instanceName).set(Number(slave.meta.tick) || 0);
}catch(e){}
}
// inventory
for (let [key, count] of db.items._items) {
prometheusMasterInventoryGauge.labels(key).set(Number(count) || 0);
// plugins
for (let plugin of masterPlugins) {
if (plugin.main && plugin.main.onMetrics && typeof plugin.main.onMetrics == "function") {
plugin.main.onMetrics();
}
}
// Slave count
let numberOfActiveSlaves = 0;
for(let slave of db.slaves.values()){
@@ -175,26 +158,10 @@ async function loadDatabase(databaseDirectory) {
let slavesPath = path.resolve(databaseDirectory, "slaves.json");
console.log(`Loading ${slavesPath}`);
db.slaves = await database.loadJsonAsMap(slavesPath);
let itemsPath = path.resolve(databaseDirectory, "items.json");
console.log(`Loading ${itemsPath}`);
try {
let content = await fs.readFile(itemsPath);
db.items = new database.ItemDatabase(JSON.parse(content));
} catch (err) {
if (err.code === "ENOENT") {
console.log("Creating new item database");
db.items = new database.ItemDatabase();
} else {
throw err;
}
}
}
// store slaves and inventory in a .json full of JSON data
// store slaves in a .json full of JSON data
async function shutdown() {
console.log('Ctrl-C...');
let exitStartTime = Date.now();
@@ -208,14 +175,6 @@ async function shutdown() {
console.error(`Slave database too large, not saving (${db.slaves.size}`);
}
if (db.items && db.items.size < 50000) {
let file = path.resolve(config.databaseDirectory, "items.json");
console.log(`writing ${file}`);
let content = JSON.stringify(db.items.serialize());
await fs.outputFile(file, content);
} else if (db.items) {
console.error(`Item database too large, not saving (${db.items.size})`);
}
for(let i in masterPlugins){
let plugin = masterPlugins[i];
if(plugin.main && plugin.main.onExit && typeof plugin.main.onExit == "function"){
@@ -410,213 +369,14 @@ app.post("/api/rconPasswords", authenticate.middleware, function(req, res) {
res.send(slaveCache.cache);
});
var recievedItemStatisticsBySlaveID = {};
var sentItemStatisticsBySlaveID = {};
//
/**
POST endpoint for storing items in master's inventory.
@memberof clusterioMaster
@instance
@alias api/place
@param {itemStack} itemStack the number and type of items to store (see typedef)
@param {string} [itemStack.instanceID="unknown"] the unique/instanceID which is a numerical value for an instance
@param {string} [itemStack.instanceName="unknown"] the name of an instance for identification in statistics, as provided when launching it. ex node client.js start [name]
@returns {string} status either "success" or "failure"
*/
app.post("/api/place", authenticate.middleware, function(req, res) {
endpointHitCounter.labels(req.route.path).inc();
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
let x;
try {
x = JSON.parse(req.body);
} catch (e) {
x = req.body;
}
if(!x.instanceName) {
x.instanceName = "unknown";
}
if(!x.instanceID) {
x.instanceID = "unknown";
for (let [instanceID, slave] of db.slaves) {
if(slave.instanceName == req.body.instanceName) {
x.instanceID = instanceID;
break;
}
}
}
if(x.instanceID
&& x.instanceName
&& !isNaN(Number(x.count))
&& x.name
&& typeof x.name == "string"){
if(config.logItemTransfers){
console.log("added: " + req.body.name + " " + req.body.count+" from "+x.instanceName+" ("+x.instanceID+")");
}
// gather statistics
let recievedItemStatistics = recievedItemStatisticsBySlaveID[x.instanceID];
if(recievedItemStatistics === undefined){
recievedItemStatistics = new averagedTimeSeries({
maxEntries: config.itemStats.maxEntries,
entriesPerSecond: config.itemStats.entriesPerSecond,
mergeMode: "add",
}, console.log);
recievedItemStatisticsBySlaveID[x.instanceID] = recievedItemStatistics;
}
recievedItemStatistics.add({
key:req.body.name,
value:req.body.count,
});
prometheusExportGauge.labels(x.instanceID, req.body.name).inc(Number(req.body.count) || 0);
// save items we get
var count = Number(req.body.count);
db.items.addItem(req.body.name, count);
// Attempt confirming
res.send("success");
} else {
res.send("failure");
}
});
const routes_api_remove = require("./routes/api/remove.js");
let neuralDole;
/**
POST endpoint to remove items from DB when client orders items.
@memberof clusterioMaster
@instance
@alias api/remove
@param {itemStack} itemStack the name of and the number of items to remove (see typedef)
@param {string} [itemStack.instanceID="unknown"] the unique/instanceID which is a numerical value for an instance
@param {string} [itemStack.instanceName="unknown"] the name of an instance for identification in statistics, as provided when launching it. ex node client.js start [name]
@returns {itemStack} the number of items actually removed, may be lower than what was asked for due to shortages.
*/
app.post("/api/remove", authenticate.middleware, function(req, res) {
endpointHitCounter.labels(req.route.path).inc();
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
// save items we get
var object = req.body;
if(!object.instanceID) {
object.instanceID = "unknown"
}
if(!object.instanceName) {
object.instanceName = "unknown";
}
if (db.slaves.has(String(object.instanceID))) {
object.instanceName = db.slaves.get(String(object.instanceID)).instanceName;
}
let itemCount = db.items.getItemCount(object.name);
if (
(config.disableImportsOfEverythingExceptElectricity === true || config.disableImportsOfEverythingExceptElectricity === "true" )
&& object.name != "electricity"
) {
if(config.logItemTransfers){
console.log('failure could not find ' + object.name);
}
res.send({name:object.name, count:0});
} else if(config.disableFairItemDistribution){
// Give out as much items as possible until there are 0 left. This might lead to one slave getting all the items and the rest nothing.
let numberToRemove = Math.min(Math.abs(Number(object.count)),itemCount);
db.items.removeItem(object.name, numberToRemove);
res.send({count: numberToRemove, name: object.name});
// track statistics and do graphing things
prometheusImportGauge.labels(object.instanceID, object.name).inc(Number(numberToRemove) || 0);
let sentItemStatistics = sentItemStatisticsBySlaveID[object.instanceID];
if(sentItemStatistics === undefined){
sentItemStatistics = new averagedTimeSeries({
maxEntries: config.itemStats.maxEntries,
entriesPerSecond: config.itemStats.entriesPerSecond,
mergeMode: "add",
}, console.log);
}
sentItemStatistics.add({
key:object.name,
value:numberToRemove,
});
//console.log(sentItemStatistics.data)
sentItemStatisticsBySlaveID[object.instanceID] = sentItemStatistics;
} else if(config.useNeuralNetDoleDivider){
// use fancy neural net to calculate a "fair" dole division rate.
neuralDole.divider({
res,
object,
config,
sentItemStatisticsBySlaveID,
prometheusImportGauge
})
} else {
// Use dole division. Makes it really slow to drain out the last little bit.
routes_api_remove.doleDivider({
itemCount,
object,
db,
sentItemStatisticsBySlaveID,
config,
prometheusDoleFactorGauge,
prometheusImportGauge,
req,res,
})
}
});
/**
GET endpoint to read the masters current inventory of items.
@memberof clusterioMaster
@instance
@alias api/inventory
@returns {object[]} JSON [{name:"iron-plate", count:100},{name:"copper-plate",count:5}]
*/
// endpoint for getting an inventory of what we got
app.get("/api/inventory", function(req, res) {
endpointHitCounter.labels(req.route.path).inc();
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
// Check it and send it
var inventory = [];
for (let [name, count] of db.items._items) {
inventory.push({ name, count });
}
res.send(JSON.stringify(inventory));
});
/**
GET endpoint to read the masters inventory as an object with key:value pairs
@memberof clusterioMaster
@instance
@alias api/inventoryAsObject
@returns {object} JSON {"iron-plate":100, "copper-plate":5}
*/
app.get("/api/inventoryAsObject", function(req, res) {
endpointHitCounter.labels(req.route.path).inc();
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
// Check it and send it
res.send(JSON.stringify(db.items.serialise()));
});
/**
GET endpoint to read the masters inventory as an object with key:value pairs
@memberof clusterioMaster
@instance
@alias api/modData
@returns {object} JSON
*/
// wrap a request in an promise
async function downloadPage(url) {
return new Promise((resolve, reject) => {
request(url, (error, response, body) => {
resolve(body);
});
async function downloadPage(url) {
return new Promise((resolve, reject) => {
request(url, (error, response, body) => {
resolve(body);
});
}
});
}
app.get("/api/modmeta", async function(req, res) {
endpointHitCounter.labels(req.route.path).inc();
@@ -832,7 +592,10 @@ async function getPlugins(){
path: pluginConfig.pluginPath,
socketio: io,
express: app,
db,
Prometheus,
prometheusPrefix,
endpointHitCounter,
}),
pluginConfig,
});
@@ -900,12 +663,6 @@ async function startServer() {
fs.writeFileSync("secret-api-token.txt", jwt.sign({ id: "api" }, config.masterAuthSecret, {
expiresIn: 86400*365 // expires in 1 year
}));
setInterval(async function() {
let file = path.resolve(config.databaseDirectory, "items.json");
console.log(`autosaving ${file}`);
let content = JSON.stringify(db.items.serialize());
await fs.outputFile(file, content);
},config.autosaveInterval || 60000);
process.on('SIGINT', shutdown); // ctrl + c
process.on('SIGHUP', shutdown); // terminal closed
@@ -921,13 +678,6 @@ async function startServer() {
await loadDatabase(config.databaseDirectory);
authenticate.setAuthSecret(config.masterAuthSecret);
// Only initialize neural network when it's enabled, otherwise it might override gauge
if(config.useNeuralNetDoleDivider) {
neuralDole=new routes_api_remove.neuralDole({
db, gaugePrefix: prometheusPrefix
});
}
// Make sure we're actually going to listen on a port
let httpPort = args.masterPort || config.masterPort;
let httpsPort = args.sslPort || config.sslPort;
+3 -5
View File
@@ -1,6 +1,6 @@
{
"name": "clusterio",
"version": "1.7.0",
"version": "2.0.0-alpha",
"description": "Interserver communication for factorio",
"repository": "https://github.com/clusterio/factorioClusterio",
"main": "master.js",
@@ -10,8 +10,8 @@
"cover": "nyc npm test",
"fast-cover": "FAST_TEST=y nyc npm test",
"ci-cover": "nyc --reporter=lcovonly npm run-script test",
"start": "echo Run node client.js start [instance] to start slaves && node master.js",
"install": "node lib/npmPostinstall.js",
"start": "echo Run node client start [instance] to start slaves && node master",
"install": "node lib/npmPostinstall",
"bundle-dependencies": "bundle-dependencies"
},
"nyc": {
@@ -53,8 +53,6 @@
"lib": "file:./lib",
"luamin": "^1.0.4",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
"mkdirp-promise": "^5.0.1",
"moment": "^2.22.2",
"needle": "^2.4.0",
"node-factorio-api": "^0.3.8",
@@ -1,6 +1,6 @@
const averagedTimeSeries = require("averaged-timeseries")
const doleNN = require("lib/dole_nn_base.js")
const doleNN = require("./dole_nn_base")
const Prometheus = require('prom-client');
@@ -19,7 +19,7 @@ class neuralDole {
}
constructor({
db, gaugePrefix
items, gaugePrefix
}){
// Set some storage variables for the dole divider
this.prometheusNNDoleGauge = new Prometheus.Gauge({
@@ -27,8 +27,8 @@ class neuralDole {
help: 'Current demand being supplied by Neural Network ; 1 means all demand covered, 0.5 means about half of each supply is covered, 0 means no items are given',
labelNames: ["itemName"],
});
this.db = db
this.itemsLastTick = new Map(db.items._items)
this.items = items
this.itemsLastTick = new Map(items._items)
this.dole = {}
this.carry = {}
this.lastRequest = {}
@@ -36,7 +36,7 @@ class neuralDole {
this.debt= {}
setInterval(()=>{
for (let [name, count] of this.db.items._items) {
for (let [name, count] of this.items._items) {
let magicData = doleNN.Tick(
count,
this.dole[name],
@@ -51,7 +51,7 @@ class neuralDole {
// DONE handle magicData[1] for graphing for our users
this.prometheusNNDoleGauge.labels(name).set(magicData[1] || 0);
}
this.itemsLastTick = new Map(this.db.items._items)
this.itemsLastTick = new Map(this.items._items)
}, 1000)
}
divider({
@@ -63,7 +63,7 @@ class neuralDole {
}){
let magicData = doleNN.Dose(
object.count, // numReq
this.db.items.getItemCount(object.name),
this.items.getItemCount(object.name),
this.itemsLastTick.get(object.name) || 0,
this.dole[object.name],
this.carry[object.name+" "+object.instanceID] || 0,
@@ -85,9 +85,9 @@ class neuralDole {
this.debt[object.name+" "+object.instanceID] = magicData[3]
// Remove item from DB and send it
this.db.items.removeItem(object.name, magicData[0]);
this.items.removeItem(object.name, magicData[0]);
if(config.logItemTransfers){
console.log("removed: " + object.name + " " + magicData[0] + " . " + this.db.items.getItemCount(object.name) + " and sent to " + object.instanceID + " | " + object.instanceName);
console.log("removed: " + object.name + " " + magicData[0] + " . " + this.items.getItemCount(object.name) + " and sent to " + object.instanceID + " | " + object.instanceName);
}
let sentItemStatistics = sentItemStatisticsBySlaveID[object.instanceID];
if(sentItemStatistics === undefined){
@@ -95,7 +95,7 @@ class neuralDole {
maxEntries: config.itemStats.maxEntries,
entriesPerSecond: config.itemStats.entriesPerSecond,
mergeMode: "add",
}, console.log);
});
}
sentItemStatistics.add({
key:object.name,
@@ -112,11 +112,12 @@ class neuralDole {
}
}
_doleDivisionFactor = {}; //If the server regularly can't fulfill requests, this number grows until it can. Then it slowly shrinks back down.
// If the server regularly can't fulfill requests, this number grows until it can. Then it slowly shrinks back down.
let _doleDivisionFactor = {};
function doleDivider({
itemCount,
object,
db,
items,
sentItemStatisticsBySlaveID,
config,
prometheusDoleFactorGauge,
@@ -140,14 +141,14 @@ function doleDivider({
if (config.logItemTransfers) {
console.log("removed: " + object.name + " " + object.count + " . " + itemCount + " and sent to " + object.instanceID + " | " + object.instanceName);
}
db.items.removeItem(object.name, object.count)
items.removeItem(object.name, object.count)
let sentItemStatistics = sentItemStatisticsBySlaveID[object.instanceID];
if(sentItemStatistics === undefined){
sentItemStatistics = new averagedTimeSeries({
maxEntries: config.itemStats.maxEntries,
entriesPerSecond: config.itemStats.entriesPerSecond,
mergeMode: "add",
}, console.log);
});
}
sentItemStatistics.add({
key:object.name,
File renamed without changes.
+1 -1
View File
@@ -1,7 +1,7 @@
const needle = require("needle");
const fs = require("fs-extra");
const objectOps = require("lib/objectOps.js");
const objectOps = require("lib/objectOps");
const fileOps = require("lib/fileOps");
const pluginConfig = require("./config");
+88 -67
View File
@@ -1,83 +1,104 @@
"use strict";
const fs = require("fs-extra");
const path = require("path");
const Express = require("express");
const ejs = require("ejs");
const database = require("lib/database");
const routes = require("./routes");
class masterPlugin {
constructor({config, pluginConfig, pluginPath, socketio, express}){
constructor({
config, pluginConfig, pluginPath, socketio, express, db, Prometheus, prometheusPrefix, endpointHitCounter,
}) {
this.config = config;
this.pluginConfig = pluginConfig;
this.pluginPath = pluginPath;
this.io = socketio;
this.app = express;
// load databases
this.items = getDatabaseSync("database/items.json");
this.clients = {};
this.slaves = {};
// initialize web API
// require("./js/api-endpoints.js")(this);
// expose UI elements embedded in the master
// this.ui = require("./js/ui.js").ui;
this.io.on("connection", socket => {
});
}
async broadcastCommand(command){
let returnValues = [];
for(let instanceID in this.slaves){
let slave = this.slaves[instanceID];
slave.emit("runCommand", {
// commandID:Math.random(),
command,
});
};
return returnValues;
}
findInArray(key, value, array){
let indexes = [];
for(let i in array){
if(array[i][key] && array[i][key] === value) indexes.push(i);
}
return indexes;
}
async onExit(){
// await saveDatabase("database/items.json", this.items);
return;
}
}
module.exports = masterPlugin;
function arrayRemoveDuplicates(array){
let newArray = [];
array.forEach(value => {
if(!newArray.includes(value)) newArray.push(value);
});
return newArray;
}
function getDatabaseSync(path){
let db;
try {
db = JSON.parse(fs.readFileSync(path, "utf8"));
} catch(e){
db = {};
this.express = express;
this.db = db,
this.Prometheus = Prometheus;
this.prometheusPrefix = prometheusPrefix;
this.endpointHitCounter = endpointHitCounter;
this.prometheusMasterInventoryGauge = new Prometheus.Gauge({
name: prometheusPrefix+'master_inventory_gauge',
help: 'Amount of items stored on master',
labelNames: ["itemName"],
});
this.ui = {
sidebar: [
{
getHtml: () => `
<div class="nav-item mr-1">
<a class="nav-link align-middle" href="/clusterioMod/storage">Storage</a>
</div>`,
},
],
};
}
onMetrics() {
if (this.items) {
for (let [key, count] of this.items._items) {
this.prometheusMasterInventoryGauge.labels(key).set(Number(count) || 0);
}
}
}
async onLoadFinish() {
this.items = await loadDatabase(this.config);
this.autosaveId = setInterval(async function() {
let file = path.join(this.config.databaseDirectory, "items.json");
console.log(`autosaving ${file}`);
let content = JSON.stringify(db.items.serialize());
await fs.outputFile(file, content);
},this.config.autosaveInterval || 60000);
routes.addApiRoutes(
this.express, this.config, this.db, this.items,
this.Prometheus, this.prometheusPrefix, this.endpointHitCounter,
);
routes.addWebRoutes(this.express);
}
async onExit(){
clearInterval(this.autosaveId);
await saveDatabase(this.config, this.items);
}
return db;
}
async function saveDatabase(path, database){
if(!path){
throw new Error("No path provided!");
} else if(!database){
throw new Error("No database provided!");
} else {
try {
await fs.writeFile(path, JSON.stringify(database, null, 4));
} catch(e){
throw new Error("Unable to write to database! "+path);
async function loadDatabase(config) {
let itemsPath = path.resolve(config.databaseDirectory, "items.json");
console.log(`Loading ${itemsPath}`);
try {
let content = await fs.readFile(itemsPath);
return new database.ItemDatabase(JSON.parse(content));
} catch (err) {
if (err.code === "ENOENT") {
console.log("Creating new item database");
return new database.ItemDatabase();
} else {
throw err;
}
}
}
async function saveDatabase(config, items) {
if (items && items.size < 50000) {
let file = path.resolve(config.databaseDirectory, "items.json");
console.log(`writing ${file}`);
let content = JSON.stringify(items.serialize());
await fs.outputFile(file, content);
} else if (items) {
console.error(`Item database too large, not saving (${items.size})`);
}
}
module.exports = masterPlugin;
+241
View File
@@ -0,0 +1,241 @@
var recievedItemStatisticsBySlaveID = {};
var sentItemStatisticsBySlaveID = {};
const averagedTimeSeries = require("averaged-timeseries");
const express = require("express");
const path = require("path");
const authenticate = require("lib/authenticate");
const dole = require("./dole");
function addWebRoutes(app) {
app.use('/clusterioMod', express.static(path.join(__dirname, 'static')));
app.get('/clusterioMod/storage', (req, res) => { res.render('clusterioMod/views/storage'); });
}
function addApiRoutes(
app, config, db, items, Prometheus, prometheusPrefix, endpointHitCounter,
) {
const prometheusExportGauge = new Prometheus.Gauge({
name: prometheusPrefix+'export_gauge',
help: 'Items exported by instance',
labelNames: ["instanceID", "itemName"],
});
const prometheusImportGauge = new Prometheus.Gauge({
name: prometheusPrefix+'import_gauge',
help: 'Items imported by instance',
labelNames: ["instanceID", "itemName"],
});
const prometheusDoleFactorGauge = new Prometheus.Gauge({
name: prometheusPrefix+'dole_factor_gauge',
help: 'The current dole division factor for this item',
labelNames: ["itemName"],
});
// Only initialize neural network when it's enabled, otherwise it might override gauge
let neuralDole;
if(config.useNeuralNetDoleDivider) {
neuralDole=new dole.neuralDole({
db, gaugePrefix: prometheusPrefix
});
}
/**
* POST endpoint for storing items in master's inventory.
* @memberof clusterioMaster
* @instance
* @alias api/place
* @param {itemStack} itemStack the number and type of items to store (see typedef)
* @param {string} [itemStack.instanceID="unknown"]
* the unique/instanceID which is a numerical value for an instance
* @param {string} [itemStack.instanceName="unknown"]
* the name of an instance for identification in statistics, as
* provided when launching it. ex node client start [name]
* @returns {string} status either "success" or "failure"
*/
app.post("/api/place", authenticate.middleware, function(req, res) {
endpointHitCounter.labels(req.route.path).inc();
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
let x;
try {
x = JSON.parse(req.body);
} catch (e) {
x = req.body;
}
if(!x.instanceName) {
x.instanceName = "unknown";
}
if(!x.instanceID) {
x.instanceID = "unknown";
for (let [instanceID, slave] of db.slaves) {
if(slave.instanceName == req.body.instanceName) {
x.instanceID = instanceID;
break;
}
}
}
if(x.instanceID
&& x.instanceName
&& !isNaN(Number(x.count))
&& x.name
&& typeof x.name == "string"){
if(config.logItemTransfers){
console.log("added: " + req.body.name + " " + req.body.count+" from "+x.instanceName+" ("+x.instanceID+")");
}
// gather statistics
let recievedItemStatistics = recievedItemStatisticsBySlaveID[x.instanceID];
if(recievedItemStatistics === undefined){
recievedItemStatistics = new averagedTimeSeries({
maxEntries: config.itemStats.maxEntries,
entriesPerSecond: config.itemStats.entriesPerSecond,
mergeMode: "add",
});
recievedItemStatisticsBySlaveID[x.instanceID] = recievedItemStatistics;
}
recievedItemStatistics.add({
key:req.body.name,
value:req.body.count,
});
prometheusExportGauge.labels(x.instanceID, req.body.name).inc(Number(req.body.count) || 0);
// save items we get
var count = Number(req.body.count);
items.addItem(req.body.name, count);
// Attempt confirming
res.send("success");
} else {
res.send("failure");
}
});
/**
* POST endpoint to remove items from DB when client orders items.
*
* @memberof clusterioMaster
* @instance
* @alias api/remove
* @param {itemStack} itemStack the name of and the number of items to remove (see typedef)
* @param {string} [itemStack.instanceID="unknown"]
* the unique/instanceID which is a numerical value for an instance
* @param {string} [itemStack.instanceName="unknown"]
* the name of an instance for identification in statistics, as
* provided when launching it. ex node client start [name]
* @returns {itemStack}
* the number of items actually removed, may be lower than what was
* asked for due to shortages.
*/
app.post("/api/remove", authenticate.middleware, function(req, res) {
endpointHitCounter.labels(req.route.path).inc();
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
// save items we get
var object = req.body;
if(!object.instanceID) {
object.instanceID = "unknown"
}
if(!object.instanceName) {
object.instanceName = "unknown";
}
if (db.slaves.has(String(object.instanceID))) {
object.instanceName = db.slaves.get(String(object.instanceID)).instanceName;
}
let itemCount = items.getItemCount(object.name);
if (
(config.disableImportsOfEverythingExceptElectricity === true || config.disableImportsOfEverythingExceptElectricity === "true" )
&& object.name != "electricity"
) {
if(config.logItemTransfers){
console.log('failure could not find ' + object.name);
}
res.send({name:object.name, count:0});
} else if(config.disableFairItemDistribution){
// Give out as much items as possible until there are 0 left. This might lead to one slave getting all the items and the rest nothing.
let numberToRemove = Math.min(Math.abs(Number(object.count)),itemCount);
items.removeItem(object.name, numberToRemove);
res.send({count: numberToRemove, name: object.name});
// track statistics and do graphing things
prometheusImportGauge.labels(object.instanceID, object.name).inc(Number(numberToRemove) || 0);
let sentItemStatistics = sentItemStatisticsBySlaveID[object.instanceID];
if(sentItemStatistics === undefined){
sentItemStatistics = new averagedTimeSeries({
maxEntries: config.itemStats.maxEntries,
entriesPerSecond: config.itemStats.entriesPerSecond,
mergeMode: "add",
});
}
sentItemStatistics.add({
key:object.name,
value:numberToRemove,
});
//console.log(sentItemStatistics.data)
sentItemStatisticsBySlaveID[object.instanceID] = sentItemStatistics;
} else if(config.useNeuralNetDoleDivider){
// use fancy neural net to calculate a "fair" dole division rate.
neuralDole.divider({
res,
object,
config,
sentItemStatisticsBySlaveID,
prometheusImportGauge
})
} else {
// Use dole division. Makes it really slow to drain out the last little bit.
dole.doleDivider({
itemCount,
object,
db,
sentItemStatisticsBySlaveID,
config,
prometheusDoleFactorGauge,
prometheusImportGauge,
req,res,
})
}
});
/**
* GET endpoint to read the masters current inventory of items.
*
* @memberof clusterioMaster
* @instance
* @alias api/inventory
* @returns {object[]} JSON [{name:"iron-plate", count:100},{name:"copper-plate",count:5}]
*/
app.get("/api/inventory", function(req, res) {
endpointHitCounter.labels(req.route.path).inc();
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
// Check it and send it
var inventory = [];
for (let [name, count] of items._items) {
inventory.push({ name, count });
}
res.send(JSON.stringify(inventory));
});
/**
* GET endpoint to read the masters inventory as an object with key:value pairs
*
* @memberof clusterioMaster
* @instance
* @alias api/inventoryAsObject
* @returns {object} JSON {"iron-plate":100, "copper-plate":5}
*/
app.get("/api/inventoryAsObject", function(req, res) {
endpointHitCounter.labels(req.route.path).inc();
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
// Check it and send it
res.send(JSON.stringify(items.serialise()));
});
}
module.exports = {
addWebRoutes,
addApiRoutes,
}
@@ -65,7 +65,7 @@ function updateInventory() {
drawcontents(data);
}
}
xmlhttp.open("GET", "api/inventory", true);
xmlhttp.open("GET", "/api/inventory", true);
xmlhttp.send();
}
if(JSON.parse(localStorage.settings)["Periodically update storage screen"]) {
@@ -3,3 +3,5 @@
require('../index');
require('../masterPlugin');
require('../dole');
require('../dole_nn_base');
+110
View File
@@ -0,0 +1,110 @@
const assert = require("assert").strict;
const express = require("express");
const bodyParser = require("body-parser");
const request = require("supertest");
const Prometheus = require('prom-client');
const jwt = require("jsonwebtoken");
const authenticate = require('lib/authenticate');
const database = require("lib/database");
const routes = require("../routes");
const testSecret = "TestSecretDoNotUse";
authenticate.setAuthSecret(testSecret);
const token = jwt.sign({ id: "api" }, testSecret, { expiresIn: 600 });
describe("clusterioMod endpoints", function() {
const app = express();
app.use(bodyParser.json({ limit: '10mb', }));
app.use(bodyParser.urlencoded({ parameterLimit: 100000, limit: '10mb', extended: true }));
const config = {
itemStats: { maxEntries:60, entriesPerSecond: 1 / 60 },
disableFairItemDistribution: true,
};
const items = new database.ItemDatabase();
const gauge = new Prometheus.Gauge({ name: 'test_gauge', help: "test", labelNames: ['route'] });
routes.addApiRoutes(app, config, { slaves: new Map() }, items, Prometheus, "test_", gauge);
let persistentMaster = request(app);
describe("#POST /api/place", function() {
it("adds an itemStack to the masters inventory", function() {
return persistentMaster.post("/api/place").send({
name:"steel-plate",
count:20,
instanceName:"unitTest"
}).set(
"X-Access-Token", token
).expect(200).then(function(res) {
assert.equal(res.text, "success", "something went wrong with the request")
});
});
});
describe("#GET /api/inventory", function() {
it("returns the masters current inventory", function() {
return persistentMaster.get("/api/inventory").expect(200).then(function(res) {
let inventory = JSON.parse(res.text);
assert.equal(typeof inventory, "object", "Inventory should be an object");
assert(inventory.length >= 1, "There should be at least 1 entry in the inventory");
let contains20SteelPlate = false;
inventory.forEach(itemStack => {
if(itemStack.name == "steel-plate" && itemStack.count >= 20) contains20SteelPlate = true;
});
assert(contains20SteelPlate, "Please ensure there are at least 20 steel plate in the inventory")
});
});
});
describe("#POST /api/remove", function() {
it("returns an itemStack of how many items were removed", function() {
return persistentMaster.post("/api/remove").send({
name:"steel-plate",
count:10
}).set(
"X-Access-Token", token
).expect(200).then(function(res) {
assert.equal(res.body.count, 10, `Expected 10 steel got ${res.text}`);
});
});
it("works correctly for items named addItem or removeItem", async function() {
function add(item) {
return persistentMaster.post("/api/place").send({
name: item,
count: 10
}).set(
"X-Access-Token", token
).expect(200).then(function(res) {
assert.equal(res.text, 'success', `/api/place ${item} failed with ${res.text}`);
})
}
function remove(item) {
return persistentMaster.post("/api/remove").send({
name: item,
count: 10
}).set(
"X-Access-Token", token
).expect(200).then(function(res) {
assert.deepEqual(res.body, {"name":item,"count":10});
});
}
await Promise.all([add("addItem"), add("removeItem")]);
await Promise.all([remove("addItem"), remove("removeItem")]);
});
it("returns an empty itemStack if you don't have any of the item you request", function() {
return persistentMaster.post("/api/remove").send({
name:"imaginaryItem",
count:999999
}).set(
"X-Access-Token", token
).expect(200).then(function(res) {
assert.equal(res.body.name, "imaginaryItem", "Make sure body.name is the item we asked for");
assert.equal(res.body.count, 0, "Count should be 0 since we are asking for something that does not exist anywhere");
assert.equal(Object.keys(res.body).length, 2, "name and count should be the only keys on this object");
});
});
});
});
@@ -0,0 +1,49 @@
<!doctype HTML>
<head>
<%- include("head") %>
<script defer src="/clusterioMod/storage.js"></script>
</head>
<body>
<%- include("nav") %>
<div id="body" class="container">
<div id="storage">
<div class="page-header mt-2 row" id="banner">
<div class="row">
<div class="col-lg-8 col-md-7 col-sm-6">
<h2>Storage</h2>
</div>
</div>
</div>
<div class="form-group mt-3 mb-3 row">
<input type="text" class="form-control" id="search" placeholder="search">
</div>
<div id="contents" class="row w-100">
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th scope="col">Icon</th>
<th scope="col">Item ID</th>
<th scope="col">Quantity</th>
</tr>
</thead>
<tbody><!-- populated by js --></tbody>
</table>
</div>
</div>
<div id="graphDisplay">
<div id="storageGraphContainer">
<div id="contentGraph" class="storageGraph" style="width: 100%;"></div>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"
integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"
crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"
integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"
crossorigin="anonymous"></script>
</body>
+2 -2
View File
@@ -1,6 +1,6 @@
const needle = require("needle");
const isJson = require("./isJson.js");
const pluginConfig = require("./config.js");
const isJson = require("./isJson");
const pluginConfig = require("./config");
const functions = require("./interfacingFunctions");
@@ -1,4 +1,4 @@
const objectOps = require("lib/objectOps.js");
const objectOps = require("lib/objectOps");
const needle = require("needle");
const pluginConfig = require("./config")
const isJson = require("./isJson")
+1 -1
View File
@@ -3,7 +3,7 @@ const nock = require("nock");
const isFactorioCommand = require("lib/isFactorioCommand");
const researchSync = require("../index.js");
const researchSync = require("../index");
describe("researchSync/index.js", function() {
it("exports a single class (or at least a function)", function() {
+1 -1
View File
@@ -8,7 +8,7 @@ module.exports = class remoteCommands {
this.messageInterface = messageInterface;
this.config = mergedConfig;
this.socket = extras.socket;
const pluginManager = require("lib/manager/pluginManager.js");
const pluginManager = require("lib/manager/pluginManager");
// const modManager = require("lib/manager/modManager");
this.socket.on("hello", () => {
this.socket.emit("registerServerManager");
+2 -2
View File
@@ -22,10 +22,10 @@ class masterPlugin {
// this.banlist = getDatabaseSync("database/banlist.json").banlist || [];
// initialize web API
require("./js/api-endpoints.js")(this);
require("./js/api-endpoints")(this);
// expose UI elements embedded in the master
this.ui = require("./js/ui.js").injectThis(this).ui;
this.ui = require("./js/ui").injectThis(this).ui;
this.serverSockets = {};
-345
View File
@@ -1,345 +0,0 @@
import spritesheetJson from "./../pictures/spritesheet.js";
export var entityDrawRules = {
"accumulator": {
spritesheet: spritesheetJson.frames["images/factorio/entity/accumulator/accumulator.png"],
sizeInTiles: {
x:3.5,
y:3.5,
},
positionOffset: {
x:-1,
y:-2,
}
},
"centrifuge": {
spritesheet: spritesheetJson.frames["images/factorio/entity/centrifuge/centrifuge-A.png.0"],
sizeInTiles: {
x:4,
y:4,
},
positionOffset: {
x:-1.5,
y:-1,
},
},
"stone-furnace": {
spritesheet: spritesheetJson.frames["images/factorio/entity/stone-furnace/stone-furnace.png"],
sizeInTiles: {
x:3,
y:2.2,
},
positionOffset: {
x:-0.9,
y:-1,
},
},
"steel-furnace": {
spritesheet: spritesheetJson.frames["images/factorio/entity/steel-furnace/steel-furnace.png"],
sizeInTiles: {
x:2.5,
y:2.5,
},
positionOffset: {
x:-1.2,
y:-1.1,
},
},
"roboport": {
spritesheet: spritesheetJson.frames["images/factorio/entity/roboport/roboport-base.png"],
sizeInTiles: {
x:5,
y:4.5,
},
positionOffset: {
x:-2,
y:-2.2,
},
},
"substation": {
spritesheet: spritesheetJson.frames["images/factorio/entity/substation/substation.png.0"],
sizeInTiles: {
x:3,
y:3,
},
positionOffset: {
x:-1,
y:-1.8,
},
},
"big-electric-pole": {
spritesheet: spritesheetJson.frames["images/factorio/entity/big-electric-pole/big-electric-pole.png.0"],
sizeInTiles: {
x:5,
y:5,
},
positionOffset: {
x:-1,
y:-3.5,
},
},
"electric-mining-drill": {
spritesheet: [{
spritesheet: spritesheetJson.frames["images/factorio/entity/electric-mining-drill/electric-mining-drill-N.png.0"],
},{
spritesheet: spritesheetJson.frames["images/factorio/entity/electric-mining-drill/electric-mining-drill-E.png.0"],
},{
spritesheet: spritesheetJson.frames["images/factorio/entity/electric-mining-drill/electric-mining-drill-S.png.0"],
},{
spritesheet: spritesheetJson.frames["images/factorio/entity/electric-mining-drill/electric-mining-drill-W.png.0"],
},
],
sizeInTiles: {
x:3,
y:3,
},
positionOffset: {
x:-1,
y:-1,
},
},
"straight-rail": {
sizeInTiles: {
x:2,
y:2,
},
positionOffset: {
x:-1,
y:-1,
}
},
};
let template3x3entity = {
sizeInTiles: {
x:3,
y:3,
},
positionOffset: {
x:-1,
y:-1,
},
};
for(let i = 1; i <= 3; i++){
let name = "assembling-machine-"+i;
// entityDrawRules[name] =
entityDrawRules[name] = {
spritesheet: spritesheetJson.frames["images/factorio/entity/"+name+"/"+name+".png.0"],
sizeInTiles: {
x:3.9,
y:3.9,
},
positionOffset: {
x:-1.4,
y:-1.2,
},
}
// template3x3entity;
};
entityDrawRules["radar"] = {
spritesheet: [],
sizeInTiles: {
x:4,
y:4,
},
positionOffset: {
x:-0.8,
y:-1.7,
},
}
for(let i = 0; i < 64; i++){
// animate the radar
entityDrawRules.radar.spritesheet.push({spritesheet: spritesheetJson.frames["images/factorio/entity/radar/radar.png."+i]});
}
["beacon", "electric-furnace"].forEach(name => {
entityDrawRules[name] = template3x3entity;
});
entityDrawRules["lab"] = {
spritesheet: spritesheetJson.frames["images/factorio/entity/lab/lab.png.0"],
sizeInTiles: {
x:3.5,
y:3,
},
positionOffset: {
x:-1,
y:-0.9,
},
}
entityDrawRules["chemical-plant"] = {
spritesheet: [{
spritesheet: spritesheetJson.frames["images/factorio/entity/chemical-plant/chemical-plant.png.0"],
sizeInTiles: {
x:3.5,
y:3.5,
},
positionOffset: {
x:-1.3,
y:-1,
},
},{
spritesheet: spritesheetJson.frames["images/factorio/entity/chemical-plant/chemical-plant.png.1"],
sizeInTiles: {
x:3.7,
y:4,
},
positionOffset: {
x:-1.3,
y:-1.5,
},
},{
spritesheet: spritesheetJson.frames["images/factorio/entity/chemical-plant/chemical-plant.png.2"],
sizeInTiles: {
x:3.5,
y:3.2,
},
positionOffset: {
x:-1.3,
y:-1,
},
},{
spritesheet: spritesheetJson.frames["images/factorio/entity/chemical-plant/chemical-plant.png.3"],
sizeInTiles: {
x:3.7,
y:3.6,
},
positionOffset: {
x:-1.45,
y:-1.3,
},
},
],
};
[{name: "solar-panel", ref:"images/factorio/entity/solar-panel/solar-panel.png"}].forEach(ent => {
entityDrawRules[ent.name] = {
spritesheet: spritesheetJson.frames[ent.ref],
sizeInTiles: {
x:3,
y:3,
},
positionOffset: {
x:-1,
y:-1,
},
}
});
["transport-belt", "fast-transport-belt", "express-transport-belt"].forEach(name => {
entityDrawRules[name] = {
spritesheet: [{
spritesheet: [ // Up
// spritesheetJson.frames["images/factorio/entity/"+name+"/"+name+".png.0"], // 0 to 31
],
sizeInTiles: {
x:1.2,y:1.4,
},
positionOffset: {
x:-0.1,y:-0,
}
},{
spritesheet: [ // Right
// spritesheetJson.frames["images/factorio/entity/"+name+"/"+name+".png.0"], // 0 to 31
],
sizeInTiles: {
x:1.2,y:1.4,
},
positionOffset: {
x:-0.1,y:-0.2,
}
},{
spritesheet: [ // Down
// spritesheetJson.frames["images/factorio/entity/"+name+"/"+name+".png.27"], // 32 to 63
],
flip: "y",
sizeInTiles: {
x:1.3,y:1.6,
},
positionOffset: {
x:-0.15,y:-1.6,
}
},{
spritesheet: [ // Left
// spritesheetJson.frames["images/factorio/entity/"+name+"/"+name+".png.27"], // 32 to 63
],
flip: "x",
sizeInTiles: {
x:1.3,y:1.6,
},
positionOffset: {
x:-1.35,y:-0.25,
}
}],
}
for(let i = 0; i < 16; i++){
if(name == "transport-belt"){
entityDrawRules[name].spritesheet[3].spritesheet.push(spritesheetJson.frames["images/factorio/entity/"+name+"/"+name+".png."+i]);
entityDrawRules[name].spritesheet[2].spritesheet.push(spritesheetJson.frames["images/factorio/entity/"+name+"/"+name+".png."+(i+16)]);
entityDrawRules[name].spritesheet[1].spritesheet.push(spritesheetJson.frames["images/factorio/entity/"+name+"/"+name+".png."+i]);
entityDrawRules[name].spritesheet[0].spritesheet.push(spritesheetJson.frames["images/factorio/entity/"+name+"/"+name+".png."+(i+16)]);
} else {
entityDrawRules[name].spritesheet[3].spritesheet.push(spritesheetJson.frames["images/factorio/entity/"+name+"/"+name+".png."+i]);
entityDrawRules[name].spritesheet[2].spritesheet.push(spritesheetJson.frames["images/factorio/entity/"+name+"/"+name+".png."+(i+32)]);
entityDrawRules[name].spritesheet[1].spritesheet.push(spritesheetJson.frames["images/factorio/entity/"+name+"/"+name+".png."+i]);
entityDrawRules[name].spritesheet[0].spritesheet.push(spritesheetJson.frames["images/factorio/entity/"+name+"/"+name+".png."+(i+32)]);
}
}
});
["splitter", "fast-splitter", "express-splitter"].forEach(splitter => {
entityDrawRules[splitter] = {
rotOffset:180,
}
});
["underground-belt", "fast-underground-belt", "express-underground-belt"].forEach(underneathie => {
entityDrawRules[underneathie] = {
rotOffset: 270,
}
});
["inserter", "fast-inserter", "stack-inserter", "filter-inserter", "burner-inserter", 'long-handed-inserter', 'stack-filter-inserter'].forEach(inserter => {
entityDrawRules[inserter] = {
rotOffset:90,
}
});
entityDrawRules["oil-refinery"] = {
spritesheet: [
{
spritesheet: spritesheetJson.frames["images/factorio/entity/oil-refinery/oil-refinery.png.0"],
}, {
spritesheet: spritesheetJson.frames["images/factorio/entity/oil-refinery/oil-refinery.png.1"],
}, {
spritesheet: spritesheetJson.frames["images/factorio/entity/oil-refinery/oil-refinery.png.2"],
}, {
spritesheet: spritesheetJson.frames["images/factorio/entity/oil-refinery/oil-refinery.png.3"],
}],
}
// correct image size for the oil refinery frames (same for all of them on this one)
entityDrawRules["oil-refinery"].spritesheet.forEach(object => {
object.sizeInTiles = {
x:10,
y:7.5,
}
object.positionOffset = {
x:-2,
y:-2.7,
}
});
// tree sizes
["tree-02",
"tree-02-red",
"tree-03",
"tree-04",
"tree-05",
"tree-07",
"tree-09",
"tree-09-brown",
"tree-09-red"].forEach(tree => {
entityDrawRules[tree] = {
sizeInTiles: {
x: 3,
y: 4,
},
positionOffset: {
x: -0.6,
y: -2.6,
}
}
});
-44
View File
@@ -7,47 +7,3 @@ export function getParameterByName(name, url) {
if(!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
// ctx, entityImages[name].img, xPos, yPos, size.x, size.y, rotation, sprWidth, sprHeight, offLeft, offTop
export function drawImageWithRotation(ctx, image, x, y, w, h, degrees, sprWidth, sprHeight, offLeft, offTop, flipX, flipY){
ctx.save();
ctx.translate(x+w/2, y+h/2);
ctx.rotate(degrees*Math.PI/180.0);
ctx.translate(-x-w/2, -y-h/2);
if(flipX) {
x -= x*2+sprWidth;
ctx.scale(-1, 1);
}
if(flipY){
y -= y*2+sprHeight;
ctx.scale(1, -1);
}
if(isImageOk(image) && sprWidth != undefined && sprHeight != undefined && offLeft != undefined && offTop != undefined){
// console.log(sprWidth+" "+sprHeight+" "+offLeft+" "+offTop+" "+w+" "+h)
ctx.drawImage(image, offLeft, offTop, sprWidth, sprHeight, x, y, w, h);
} else if(isImageOk(image)){
ctx.drawImage(image, x, y, w, h);
} else {
// console.log("Image is not OK! "+image.src);
}
ctx.restore();
}
export function isImageOk(img) {
// During the onload event, IE correctly identifies any images that
// weren't downloaded as not complete. Others should too. Gecko-based
// browsers act like NS4 in that they report this incorrectly.
if (!img.complete) {
return false;
}
// However, they do have two very useful properties: naturalWidth and
// naturalHeight. These give the true size of the image. If it failed
// to load, either of these should be zero.
if (typeof img.naturalWidth != "undefined" && img.naturalWidth == 0) {
return false;
}
// No other way of checking: assume it's ok.
return true;
}
+8
View File
@@ -176,6 +176,10 @@ describe("lib/database", function() {
() => items.addItem('a', NaN),
new Error("count must be a number")
);
assert.throws(
() => items.addItem('a', "1"),
new Error("count must be a number")
);
});
});
@@ -206,6 +210,10 @@ describe("lib/database", function() {
() => items.removeItem('a', 'b'),
new Error("count must be a number")
);
assert.throws(
() => items.removeItem('a', "1"),
new Error("count must be a number")
);
});
});
});
+2 -2
View File
@@ -2,7 +2,7 @@ var assert = require("assert");
var fs = require("fs-extra");
var fileOps = require("lib/fileOps");
var generateSSLcert = require("lib/generateSSLcert.js")
var generateSSLcert = require("lib/generateSSLcert")
describe("generateSSLcert.js(options)", ()=>{
it("Creates a folder with a .crt and .key file in it", async function() {
@@ -12,7 +12,7 @@ describe("generateSSLcert.js(options)", ()=>{
assert(!await fs.exists(certPath)); // if it already exsits, the test might pass because of leftover data (which is bad)
await generateSSLcert({
// bits:512, // supported, but I just use the default of 2048 for this test
bits: 512, // This is too small for real world usage, but faster to test
sslCertPath: certPath,
sslPrivKeyPath: privKeyPath,
doLogging: false,
+1 -91
View File
@@ -2,25 +2,14 @@ const assert = require('assert').strict;
const request = require('supertest');
const validateHTML = require('html5-validator');
const parallel = require('mocha.parallel');
const jwt = require("jsonwebtoken");
const master = require('../master.js');
const authenticate = require('lib/authenticate');
const database = require('lib/database');
const master = require('../master');
master._db.slaves = new Map();
master._db.items = new database.ItemDatabase();
// Fields required in the config
master._config.factorioDirectory = "factorio";
master._config.itemStats = { maxEntries:60, entriesPerSecond: 1 / 60 };
master._config.disableFairItemDistribution = true;
const testSecret = "TestSecretDoNotUse";
authenticate.setAuthSecret(testSecret);
const token = jwt.sign({ id: "api" }, testSecret, { expiresIn: 600 });
async function get(path) {
return await request(master.app).get(path).expect(200);
@@ -54,83 +43,4 @@ describe('Master testing', function() {
it(`sends some HTML when accessing ${path}`, () => getValidate(path));
}
});
let persistentMaster = request(master.app);
describe("#POST /api/place", function() {
it("adds an itemStack to the masters inventory", function() {
return persistentMaster.post("/api/place").send({
name:"steel-plate",
count:20,
instanceName:"unitTest"
}).set(
"X-Access-Token", token
).expect(200).then(function(res) {
assert.equal(res.text, "success", "something went wrong with the request")
});
});
});
describe("#GET /api/inventory", function() {
it("returns the masters current inventory", function() {
return persistentMaster.get("/api/inventory").expect(200).then(function(res) {
let inventory = JSON.parse(res.text);
assert.equal(typeof inventory, "object", "Inventory should be an object");
assert(inventory.length >= 1, "There should be at least 1 entry in the inventory");
let contains20SteelPlate = false;
inventory.forEach(itemStack => {
if(itemStack.name == "steel-plate" && itemStack.count >= 20) contains20SteelPlate = true;
});
assert(contains20SteelPlate, "Please ensure there are at least 20 steel plate in the inventory")
});
});
});
describe("#POST /api/remove", function() {
it("returns an itemStack of how many items were removed", function() {
return persistentMaster.post("/api/remove").send({
name:"steel-plate",
count:10
}).set(
"X-Access-Token", token
).expect(200).then(function(res) {
assert.equal(res.body.count, 10, `Expected 10 steel got ${res.text}`);
});
});
it("works correctly for items named addItem or removeItem", async function() {
function add(item) {
return persistentMaster.post("/api/place").send({
name: item,
count: 10
}).set(
"X-Access-Token", token
).expect(200).then(function(res) {
assert.equal(res.text, 'success', `/api/place ${item} failed with ${res.text}`);
})
}
function remove(item) {
return persistentMaster.post("/api/remove").send({
name: item,
count: 10
}).set(
"X-Access-Token", token
).expect(200).then(function(res) {
assert.deepEqual(res.body, {"name":item,"count":10});
});
}
await Promise.all([add("addItem"), add("removeItem")]);
await Promise.all([remove("addItem"), remove("removeItem")]);
});
it("returns an empty itemStack if you don't have any of the item you request", function() {
return persistentMaster.post("/api/remove").send({
name:"imaginaryItem",
count:999999
}).set(
"X-Access-Token", token
).expect(200).then(function(res) {
assert.equal(res.body.name, "imaginaryItem", "Make sure body.name is the item we asked for");
assert.equal(res.body.count, 0, "Count should be 0 since we are asking for something that does not exist anywhere");
assert.equal(Object.keys(res.body).length, 2, "name and count should be the only keys on this object");
});
});
});
});
-1
View File
@@ -2,7 +2,6 @@
// required here in order to track coverage for them.
require('lib/clusterTools');
require('lib/dole_nn_base');
require('lib/getInstances');
require('lib/manager/instanceManager');
require('lib/manager/modManager');
+1 -33
View File
@@ -1,7 +1,6 @@
<!doctype HTML>
<head>
<%- include("head") %>
<script defer src="storage.js"></script>
</head>
<body>
@@ -10,40 +9,9 @@
<div id="storage">
<div class="page-header mt-2 row" id="banner">
<div class="row">
<div class="col-lg-8 col-md-7 col-sm-6">
<h2>Storage</h2>
</div>
<h2>Work in progress interface</h2>
</div>
</div>
<div class="form-group mt-3 mb-3 row">
<input type="text" class="form-control" id="search" placeholder="search">
</div>
<div id="contents" class="row w-100">
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th scope="col">Icon</th>
<th scope="col">Item ID</th>
<th scope="col">Quantity</th>
</tr>
</thead>
<tbody><!-- populated by js --></tbody>
</table>
</div>
</div>
<div id="graphDisplay">
<div id="storageGraphContainer">
<div id="contentGraph" class="storageGraph" style="width: 100%;"></div>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"
integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"
crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"
integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"
crossorigin="anonymous"></script>
</body>
-3
View File
@@ -5,9 +5,6 @@
<h3 class=" ml-3 nav-title">Clusterio</h3>
</a>
</div>
<div class="nav-item mr-1">
<a class="nav-link align-middle" href="/">Storage</a>
</div>
<div class="nav-item mr-1">
<a class="nav-link align-middle" href="/nodes">Slaves</a>
</div><%