mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-13 07:04:53 +09:00
34 files changed
+595
-946
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
```
|
||||
@@ -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'));
|
||||
|
||||
@@ -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 @@
|
||||
module.exports = require("./isFactorioCommand.js");
|
||||
module.exports = require("./isFactorioCommand");
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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,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");
|
||||
|
||||
@@ -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;
|
||||
@@ -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');
|
||||
@@ -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>
|
||||
@@ -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")
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,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
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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>
|
||||
@@ -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><%
|
||||
|
||||
Reference in new issue
Block a user