diff --git a/README.md b/README.md index 2a05b561..2d03c6d4 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ There are no requirements for other mods, they can be ran in any configuration a You can change the name and path expected in config.json if wanted. -3. node client.js [instancename] +3. node client.js start [instancename] Repeat step 3 for more servers on one machine. You should be able to find its port by looking at the console output or at the slave section on master:8080 @@ -59,3 +59,25 @@ Repeat step 3 for more servers on one machine. You should be able to find its po 2. Drop it into ./factorio/mods 3. Run factorio and connect to slave as a normal MP game. + +#Ubuntu setup + +Master and all slaves: + + sudo curl -sL https://deb.nodesource.com/setup_7.x | sudo -E bash - && sudo apt install -y git nodejs && git clone https://github.com/Danielv123/factorioClusterio.git && cd factorioClusterio && npm install + +downloads and installs nodejs, git and clusterio. + +**Master** + + node master.js + +**Slave** + +Download factorio with curl. Make sure to insert the correct version number. + +Start the slave with node client.js + + curl -o factorio.tar.gz -L https://www.factorio.com/get-download/0.14.21/headless/linux64 && tar -xvzf factorio.tar.gz + + node client.js start [instancename] diff --git a/client.js b/client.js index a7c8ee97..5d35369e 100644 --- a/client.js +++ b/client.js @@ -1,42 +1,83 @@ var fs = require('fs'); var needle = require("needle"); var child_process = require('child_process'); +var path = require('path') // require config.json var config = require('./config'); +// Functions +var deleteFolderRecursive = function (path) { + if (fs.existsSync(path)) { + fs.readdirSync(path).forEach(function (file, index) { + var curPath = path + "/" + file; + if (fs.lstatSync(curPath).isDirectory()) { // recurse + deleteFolderRecursive(curPath); + } else { // delete file + fs.unlinkSync(curPath); + } + }); + fs.rmdirSync(path); + } +}; -var instance = process.argv[2]; -if(!instance){ - console.error("Usage: node client.js [instance name]") +function getDirectories(srcpath) { + return fs.readdirSync(srcpath).filter(function (file) { + return fs.statSync(path.join(srcpath, file)).isDirectory(); + }); +} +if (!fs.existsSync("./instances/")) { + fs.mkdirSync("instances"); +} +var instance = process.argv[3]; +var instancedirectory = './instances/' + instance; +var command = process.argv[2]; +// handle commandline parameters +if (!command || command == "help" || command == "--help") { + console.error("Usage: ") + console.error("node client.js start [instance name]") + console.error("node client.js list") + console.error("node client.js delete [instance]") process.exit(1) -} -if(!fs.existsSync("./instances/")){ - fs.mkdirSync("instances"); -} -var instancedirectory = './instances/'+instance; -if (!fs.existsSync(instancedirectory)){ - fs.mkdirSync(instancedirectory); - fs.mkdirSync(instancedirectory + "/script-output/"); - fs.writeFileSync(instancedirectory + "/script-output/output.txt", "") - fs.writeFileSync(instancedirectory + "/script-output/orders.txt", "") - fs.writeFileSync(instancedirectory + "/script-output/txbuffer.txt", "") - fs.mkdirSync(instancedirectory + "/mods/") - fs.symlinkSync('../../../clusterio_0.0.3',instancedirectory + "/mods/clusterio_0.0.3",'junction') - fs.writeFileSync(instancedirectory + "/config.ini", "[path]\r\n\ +} else if (command == "list") { + console.dir(getDirectories("./instances/")); + process.exit(1) +} else if (command == "delete") { + if (!process.argv[3]) { + console.error("Usage: node client.js delete [instance]"); + process.exit(1) + } else if (typeof process.argv[3] == "string" && fs.existsSync("./instances/" + process.argv[3]) && process.argv[3] != "/" && process.argv[3] != "") { + deleteFolderRecursive("./instances/" + process.argv[3]); + console.log("Deleted instance " + process.argv[3]) + process.exit(1) + } else { + console.error("Instance not found: " + process.argv[3]); + process.exit(1) + } +} else if (command == "start" && typeof instance == "string" && instance != "/" && !fs.existsSync(instancedirectory)) { + // if instance does not exist, create it + console.log("Creating instance...") + fs.mkdirSync(instancedirectory); + fs.mkdirSync(instancedirectory + "/script-output/"); + fs.writeFileSync(instancedirectory + "/script-output/output.txt", "") + fs.writeFileSync(instancedirectory + "/script-output/orders.txt", "") + fs.writeFileSync(instancedirectory + "/script-output/txbuffer.txt", "") + fs.mkdirSync(instancedirectory + "/mods/") + fs.symlinkSync('../../../clusterio_0.0.3', instancedirectory + "/mods/clusterio_0.0.3", 'junction') + fs.writeFileSync(instancedirectory + "/config.ini", "[path]\r\n\ read-data=__PATH__executable__\\..\\..\\data\r\n\ -write-data=__PATH__executable__\\..\\..\\..\\instances\\"+instance+"\r\n\ +write-data=__PATH__executable__\\..\\..\\..\\instances\\" + instance + "\r\n\ "); var instconf = { - "factorioPort": Math.floor(Math.random()*65535), - "clientPort": Math.floor(Math.random()*65535), + "factorioPort": Math.floor(Math.random() * 65535), + "clientPort": Math.floor(Math.random() * 65535), "clientPassword": Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 8), } console.log(instconf) fs.writeFileSync(instancedirectory + "/config.json", JSON.stringify(instconf, null, 4)); var serversettings = { - "name": config.username +"'s clusterio "+instance, + "name": config.username + "'s clusterio " + instance, "description": config.description, "tags": ["clusterio"], "max_players": "20", @@ -45,60 +86,58 @@ write-data=__PATH__executable__\\..\\..\\..\\instances\\"+instance+"\r\n\ "token": config.usertoken, "game_password": config.game_password, "verify_user_identity": config.verify_user_identity, - "admins":[config.username], - "allow_commands":config.allow_commands, - "autosave_interval":10, - "autosave_slots":5, - "afk_autokick_interval":0, - "auto_pause":config.auto_pause + "admins": [config.username], + "allow_commands": config.allow_commands, + "autosave_interval": 10, + "autosave_slots": 5, + "afk_autokick_interval": 0, + "auto_pause": config.auto_pause } fs.writeFileSync(instancedirectory + "/server-settings.json", JSON.stringify(serversettings, null, 4)); var createSave = child_process.spawnSync( - './' + config.factorioDirectory + '/bin/x64/factorio.exe', - [ - '-c',instancedirectory+'/config.ini', - '--create',instancedirectory+'/save.zip', + './' + config.factorioDirectory + '/bin/x64/factorio', [ + '-c', instancedirectory + '/config.ini', + '--create', instancedirectory + '/save.zip', ] ) + console.log("Instance created!") } -var instanceconfig = require(instancedirectory+'/config'); +var instanceconfig = require(instancedirectory + '/config'); -process.on('SIGINT', function() { - console.log("Caught interrupt signal"); - //serverprocess.stdin.write('/quit') +process.on('SIGINT', function () { + console.log("Caught interrupt signal"); + //serverprocess.stdin.write('/quit') }); //var serverprocess = child_process.exec(commandline) var serverprocess = child_process.spawn( - './' + config.factorioDirectory + '/bin/x64/factorio.exe', - [ - '-c',instancedirectory+'/config.ini', - '--start-server',instancedirectory+'/save.zip', - '--rcon-port',instanceconfig.clientPort, - '--rcon-password',instanceconfig.clientPassword, - '--server-settings',instancedirectory+'/server-settings.json', - '--port',instanceconfig.factorioPort - ], - { - 'stdio':['pipe','pipe','pipe'] - } + './' + config.factorioDirectory + '/bin/x64/factorio', [ + '-c', instancedirectory + '/config.ini', + '--start-server', instancedirectory + '/save.zip', + '--rcon-port', instanceconfig.clientPort, + '--rcon-password', instanceconfig.clientPassword, + '--server-settings', instancedirectory + '/server-settings.json', + '--port', instanceconfig.factorioPort + ], { + 'stdio': ['pipe', 'pipe', 'pipe'] + } ) serverprocess.on('close', (code) => { - console.log(`child process exited with code ${code}`); - process.exit(); + console.log(`child process exited with code ${code}`); + process.exit(); }); serverprocess.stdout.on('data', (chunk) => { - console.log('OUT: '+chunk); + console.log('OUT: ' + chunk); }) serverprocess.stderr.on('data', (chunk) => { - console.log('ERR: '+chunk); + console.log('ERR: ' + chunk); }) // connect us to the server with rcon @@ -113,14 +152,16 @@ var client = new Rcon({ // wait a few seconds to let the server finish starting before connecting rcon //TODO: catch '2.033 Info RemoteCommandProcessor.cpp:97: Starting RCON interface at port 35002' from stdout maybe? -setTimeout(() => { client.connect(); }, 5000); +setTimeout(() => { + client.connect(); +}, 5000); -client.on('authenticated', function() { +client.on('authenticated', function () { console.log('Authenticated!'); -}).on('connected', function() { +}).on('connected', function () { console.log('Connected!'); getID(); -}).on('disconnected', function() { +}).on('disconnected', function () { console.log('Disconnected!'); // now reconnect client.connect(); @@ -133,117 +174,133 @@ lastSignalCheck = Date.now(); // world IDs ------------------------------------------------------------------ function getID() { var payload = { - time : Date.now(), - rconPort : instanceconfig.clientPort, - rconPassword : instanceconfig.clientPassword, - serverPort : instanceconfig.factorioPort, - unique : instanceconfig.clientPassword.hashCode() + time: Date.now(), + rconPort: instanceconfig.clientPort, + rconPassword: instanceconfig.clientPassword, + serverPort: instanceconfig.factorioPort, + unique: instanceconfig.clientPassword.hashCode() } - require('getmac').getMac(function(err,mac){ + require('getmac').getMac(function (err, mac) { if (err) throw err payload.mac = mac console.log(payload) - needle.post(config.masterIP + ":" + config.masterPort + '/getID', payload, function(err, response, body){ - if(response && response.body) { + needle.post(config.masterIP + ":" + config.masterPort + '/getID', payload, function (err, response, body) { + if (response && response.body) { // In the future we might be interested in whether or not we actually manage to send it, but honestly I don't care. console.log(response.body) } - } ); + }); }) - } -// provide items -------------------------------------------------------------- -// trigger when something happens to output.txt -fs.watch(instancedirectory + "/script-output/output.txt", function(eventType, filename) { - // get array of lines in file - items = fs.readFileSync(instancedirectory + "/script-output/output.txt", "utf8").split("\n"); - // if you found anything, reset the file - if(items[0]) { - fs.writeFileSync(instancedirectory + "/script-output/output.txt", "") - } - for(i = 0;i < items.length; i++) { - if(items[i]) { - g = items[i].split(" "); - g[0] = g[0].replace("\u0000", ""); - // console.log("exporting " + JSON.stringify(g)); - // send our entity and count to the master for him to keep track of - needle.post(config.masterIP + ":" + config.masterPort + '/place', {name:g[0], count:g[1]}, - function(err, resp, body){ - // console.log(body); - }); +setInterval(getID, 10000) + // provide items -------------------------------------------------------------- + // trigger when something happens to output.txt +fs.watch(instancedirectory + "/script-output/output.txt", function (eventType, filename) { + // get array of lines in file + items = fs.readFileSync(instancedirectory + "/script-output/output.txt", "utf8").split("\n"); + // if you found anything, reset the file + if (items[0]) { + fs.writeFileSync(instancedirectory + "/script-output/output.txt", "") } - } -}) -// request items -------------------------------------------------------------- -setInterval(function() { - // get array of lines in file - items = fs.readFileSync(instancedirectory + "/script-output/orders.txt", "utf8").split("\n"); - // if we actually got anything from the file, proceed and reset file - if(items[0]) { - fs.writeFileSync(instancedirectory + "/script-output/orders.txt", ""); - // prepare a package of all our requested items in a more tranfer friendly format - var preparedPackage = {}; - for(i = 0;i < items.length; i++) { - (function(i){ - if(items[i]) { - items[i] = items[i].split(" "); - items[i][0] = items[i][0].replace("\u0000", ""); - items[i][0] = items[i][0].replace(",", ""); - if(preparedPackage[items[i][0]]){ - if(typeof Number(preparedPackage[items[i][0]].count) == "number" && typeof Number(items[i][1]) == "number") { - preparedPackage[items[i][0]] = {"name":items[i][0], "count":Number(preparedPackage[items[i][0]].count) + Number(items[i][1])}; - } else if (typeof Number(items[i][1]) == "number") { - preparedPackage[items[i][0]] = {"name":items[i][0], "count":Number(items[i][1])}; - } - } else if (typeof Number(items[i][1]) == "number") { - preparedPackage[items[i][0]] = {"name":items[i][0], "count":Number(items[i][1])}; - } - } - })(i); - } - // request our items, one item at a time - for(i = 0;i Clusterio - + @@ -17,9 +17,9 @@ -

Slaves

diff --git a/static/main.js b/static/main.js index 86b02406..5c63abe6 100644 --- a/static/main.js +++ b/static/main.js @@ -1,9 +1,10 @@ - +var g = {} contents = { "iron-plate":100, "copper-plate":7312, } // nice functions +// hash a string to a hash function djb2(str){ var hash = 5381; for (var i = 0; i < str.length; i++) { @@ -11,7 +12,7 @@ function djb2(str){ } return hash; } - +// hash a string to a color function hashColor(str) { var hash = djb2(str); var r = (hash & 0xFF0000) >> 16; @@ -19,21 +20,23 @@ function hashColor(str) { var b = hash & 0x0000FF; return "#" + ("0" + r.toString(16)).substr(-2) + ("0" + g.toString(16)).substr(-2) + ("0" + b.toString(16)).substr(-2); } - +// exactly what you would expect it to, returns String function capitalizeFirstLetter(string) { return string.charAt(0).toUpperCase() + string.slice(1); } -// function to draw data we recieve from ajax requests +// hide the HTML object passed as argument #1 function hideThis(object) { object.style.visibility = "hidden"; } +// function to sort arrays of objects after a keys value function sortByKey(array, key) { return array.sort(function(a, b) { var x = Number(a[key]); var y = Number(b[key]); return ((x < y) ? 1 : ((x > y) ? -1 : 0)); }); } +// function to draw data we recieve from ajax requests function drawcontents(data) { var data = sortByKey(data, "count"); result = ""; @@ -65,88 +68,126 @@ function display(page) { } } -// Function to redraw charts in case they bug out -function drawcharts() { - // create chart of items in master storage - ctx = document.querySelector("#contentGraph").getContext('2d'); - PieChart = new Chart(ctx); - - // production chart - // https://codepen.io/statuswoe/pen/hyldD - var count = 20; - var data = { - labels : ["1","2","3","4","5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20"], - datasets : [ - { - fillColor : "rgba(220,220,220,0.5)", - strokeColor : "rgba(220,220,220,1)", - pointColor : "rgba(220,220,220,1)", - pointStrokeColor : "#fff", - title:"one", - data : [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] - }, - { - fillColor : "rgba(151,187,205,0.5)", - strokeColor : "rgba(151,187,205,1)", - pointColor : "rgba(151,187,205,1)", - pointStrokeColor : "#fff", - title:"two", - data : [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] - } - ] - } - // this is ugly, don't judge me - var updateData = function(oldData){ - if(piedataOld[0]){ - //console.log(piedata) - var labels = oldData["labels"]; - var dataSetA = oldData["datasets"][0]["data"]; - var dataSetB = oldData["datasets"][1]["data"]; - - labels.shift(); - count++; - labels.push(count.toString()); - // this is where we generate new data - var newDataA = piedata[0].value - piedataOld[0].value; - var newDataB = piedata[1].value - piedataOld[1].value; - // not quite sure how this works, where is dataSetA placed in data/the graph? - dataSetA.push(newDataA); - dataSetB.push(newDataB); - dataSetA.shift(); - dataSetB.shift(); - } - }; - - // Not sure why the scaleOverride isn't working... - var optionsNoAnimation = { - animation : false, - //Boolean - If we want to override with a hard coded scale - scaleOverride : true, - //** Required if scaleOverride is true ** - //Number - The number of steps in a hard coded scale - scaleSteps : 20, - //Number - The value jump in the hard coded scale - scaleStepWidth : 10, - //Number - The scale starting value - scaleStartValue : 0 - } - - //Get the context of the canvas element we want to select - var ctx = document.getElementById("productionChart").getContext("2d"); - var optionsNoAnimation = {animation : false} - productionChart = new Chart(ctx); - productionChart.Line(data, optionsNoAnimation); - - setInterval(function(){ - updateData(data); - productionChart.Line(data, optionsNoAnimation); - },1000); -} - // setTimeout is used to put this processing in the back of the queue, after the HTML canvas is done -setTimeout(function(){ - drawcharts(); -},10) +window.onload = function () { + display("storage") + // dataPoints + var dataPoints1 = []; + var dataPoints2 = []; + + var chart = new CanvasJS.Chart("productionChart",{ + zoomEnabled: true, + title: { + text: "Production" + }, + toolTip: { + shared: true + }, + legend: { + verticalAlign: "top", + horizontalAlign: "center", + fontSize: 14, + fontWeight: "bold", + fontFamily: "calibri", + fontColor: "dimGrey" + }, + axisX: { + title: "Shows items in network" + }, + axisY:{ + prefix: '', + includeZero: true + }, + data: [{ + // dataSeries1 + type: "line", + xValueType: "dateTime", + showInLegend: true, + name: "Company A", + dataPoints: dataPoints1 + }, + { + // dataSeries2 + type: "line", + xValueType: "dateTime", + showInLegend: true, + name: "Company B" , + dataPoints: dataPoints2 + }], + legend:{ + cursor:"pointer", + itemclick : function(e) { + if (typeof(e.dataSeries.visible) === "undefined" || e.dataSeries.visible) { + e.dataSeries.visible = false; + } + else { + e.dataSeries.visible = true; + } + chart.render(); + } + } + }); + + + + var updateInterval = 10000; + // initial value + var yValue1 = 640; + var yValue2 = 604; + + var time = new Date; + time.setHours(9); + time.setMinutes(30); + time.setSeconds(00); + time.setMilliseconds(00); + // starting at 9.30 am + + var updateChart = function (count) { + count = count || 1; + + // count is number of times loop runs to generate random dataPoints. + + for (var i = 0; i < count; i++) { + + // add interval duration to time + time.setTime(time.getTime()+ updateInterval); + + + // generating random values + var deltaY1 = .5 + Math.random() *(-.5-.5); + var deltaY2 = .5 + Math.random() *(-.5-.5); + + // adding random value and rounding it to two digits. + yValue1 = Math.round((yValue1 + deltaY1)*100)/100; + yValue2 = Math.round((yValue2 + deltaY2)*100)/100; + + // pushing the new values + dataPoints1.push({ + x: time.getTime(), + y: yValue1 + }); + dataPoints2.push({ + x: time.getTime(), + y: yValue2 + }); + + + }; + + // updating legend text with updated with y Value + chart.options.data[0].legendText = " Company A $" + yValue1; + chart.options.data[1].legendText = " Company B $" + yValue2; + + chart.render(); + + }; + + // generates first set of dataPoints + updateChart(3000); + + // update chart after specified interval + setInterval(function(){updateChart()}, updateInterval); +} var piedata = {}; var piedataOld = {}; @@ -172,40 +213,41 @@ setInterval(function() { label: data[i].name, } } - PieChart.Pie(piedata, { - animation: false, - legend: { - display: true, - labels: { - fontColor: 'rgb(255, 99, 132)' - } - }, - hover: { - mode: "label", - } - }); } } xmlhttp.open("GET", "inventory", true); xmlhttp.send(); }, 500) // get all slaves recently connected to master -Date.prototype.yyyymmdd = function() { // http://stackoverflow.com/questions/3066586/get-string-in-yyyymmdd-format-from-js-date-object +Date.prototype.yyyymmdd = function(time) { // http://stackoverflow.com/questions/3066586/get-string-in-yyyymmdd-format-from-js-date-object var mm = this.getMonth() + 1; // getMonth() is zero-based var dd = this.getDate(); - return [this.getFullYear(), !mm[1] && '0', mm, !dd[1] && '0', dd].join(''); // padding + if(mm<10)mm = "0"+mm + if(dd<10)dd = "0"+dd + console.log(this.getFullYear()) + return this.getFullYear()+""+mm+""+dd+ ""; // padding }; var date = new Date(); + +// ask master for slaves setInterval(function() { var xmlhttp = new XMLHttpRequest(); xmlhttp.onreadystatechange = function() { if(xmlhttp.readyState == 4 && xmlhttp.status == 200) { var slaveData = JSON.parse(xmlhttp.responseText); - var HTML = "

Latest connections:

" + var HTML = "

Current connections:

" for(i=0;i

Last seen: "+date.yyyymmdd(slaveData[key].time)+"

Port: "+slaveData[key].serverPort+"

Host: "+slaveData[key].mac+"

" + if(Date.now() - slaveData[key].time < 12000) { + // Display ISO 6801 compliant date to please Zarthus + // maybe include an option to use y-ymd-ymd-y for Trangar compatibility as well + var seenDate = date.yyyymmdd(slaveData[key].time) + if(g.trangarTime == true){ + var seenDate = (seenDate+"")[0]+"-"+(seenDate+"")[1]+(seenDate+"")[4]+(seenDate+"")[6]+"-"+(seenDate+"")[2]+(seenDate+"")[5]+(seenDate+"")[7]+"-"+(seenDate+"")[3] + } + HTML += "

ID: " + slaveData[key].unique + "

Last seen: "+seenDate+"

Port: "+slaveData[key].serverPort+"

Host: "+slaveData[key].mac+"

" + } } document.querySelector("#slaves > #display").innerHTML = HTML } @@ -216,6 +258,7 @@ setInterval(function() { // image data // key is the name of the item in the database, value is the name of the image on wiki.factorio.com/images/* +// this list has to include all entities that doesn't just follow the simple capitalize first letter convention var imagedata = { ["empty-barrel"]: "Barrel-empty", ["transport-belt"]: "Basic-transport-belt", diff --git a/static/style.css b/static/style.css index d2a2fc76..13e5a73c 100644 --- a/static/style.css +++ b/static/style.css @@ -19,6 +19,9 @@ #body > div { display:none; } +#body > #production { + display:block; +} #contents { width:300px; }