diff --git a/.gitignore b/.gitignore index 8e02f843..e9ddfb74 100644 --- a/.gitignore +++ b/.gitignore @@ -50,5 +50,6 @@ sharedPlugins/* !sharedPlugins/remoteCommands/ !sharedPlugins/researchSync/ !sharedPlugins/globalChat/ +!sharedPlugins/clusterioMod/ \.cache-require-paths\.json diff --git a/client.js b/client.js index 2d3e8592..35097d3a 100644 --- a/client.js +++ b/client.js @@ -229,7 +229,7 @@ if (!command || command == "help" || command == "--help") { (async function(){try{ const pluginManager = require("./lib/manager/pluginManager.js")(config); if(action == "list"){ - console.log(await pluginManager.listPlugins()) + await pluginManager.listPlugins(); } else if(action == "add" || action == "install" || action == "download"){ let status = await pluginManager.addPlugin(process.argv[6]); } else if(action == "remove" || action == "uninstall" || action == "delete"){ @@ -780,262 +780,6 @@ function instanceManagement() { }); } } - - // flow/production statistics ------------------------------------------------------------ - oldFlowStats = false - setInterval(function(){ - fs.readFile(instancedirectory + "/script-output/flows.txt", {encoding: "utf8"}, function(err, data) { - if(!err && data) { - let timestamp = Date.now(); - data = data.split("\n"); - let flowStats = []; - for(let i = 0; i < data.length; i++) { - // try catch to remove any invalid json - try{ - flowStats[flowStats.length] = JSON.parse(data[i]); - } catch (e) { - // console.log(" invalid json: " + i); - // some lines of JSON are invalid but don't worry, we just filter em out - } - } - // fluids - let flowStat1 = flowStats[flowStats.length-1].flows.player.input_counts - // items - let flowStat2 = flowStats[flowStats.length-2].flows.player.input_counts - // merge fluid and item flows - let totalFlows = {}; - for(let key in flowStat1) totalFlows[key] = flowStat1[key]; - for(let key in flowStat2) totalFlows[key] = flowStat2[key]; - if(oldFlowStats && totalFlows && oldTimestamp) { - let payload = objectOps.deepclone(totalFlows); - // change from total reported to per time unit - for(let key in oldFlowStats) { - // get production per minute - payload[key] = Math.floor((payload[key] - oldFlowStats[key])/(timestamp - oldTimestamp)*60000); - if(payload[key] < 0) { - payload[key] = 0; - } - } - for(let key in payload) { - if(payload[key] == '0') { - delete payload[key]; - } - } - console.log("Recorded flows, copper plate since last time: " + payload["copper-plate"]); - needle.post(config.masterIP + ":" + config.masterPort + '/api/logStats', {timestamp: timestamp, instanceID: instanceconfig.unique,data: payload}, needleOptionsWithTokenAuthHeader, function (err, response, body) { - // we did it, keep going - }); - } - oldTimestamp = timestamp; - oldFlowStats = totalFlows; - fs.writeFileSync(instancedirectory + "/script-output/flows.txt", ""); - } - }); - // we don't need to update stats quickly as that could be expensive - }, 60000*5); - - // 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 - let 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 (let i = 0; i < items.length; i++) { - if (items[i]) { - let 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 + '/api/place', { - name: g[0], - count: g[1], - instanceName: instance, // name of instance - instanceID: instanceconfig.unique, // a hash computed from the randomly generated rcon password - }, needleOptionsWithTokenAuthHeader, function (err, resp, body) { - if(body == "failure") console.error("#### Export failed! Lost: "+g[1]+" "+g[0]); - if(config.logItemTransfers){ - if(body == "success") console.log(`Exported ${g[1]} ${g[0]} to master`); - } - }); - } - } - }); - // request items -------------------------------------------------------------- - setInterval(function () { - // get array of lines in file - let 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 (let 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 we have buffered some already, sum the new items - 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]), - "instanceName":instance, - "instanceID":instanceconfig.unique, - }; - // else just add em in without summing - } else if (typeof Number(items[i][1]) == "number") { - preparedPackage[items[i][0]] = { - "name": items[i][0], - "count": Number(items[i][1]), - "instanceName":instance, - "instanceID":instanceconfig.unique, - }; - } - // this condition will NEVER be triggered but we know how that goes - } else if (typeof Number(items[i][1]) == "number") { - preparedPackage[items[i][0]] = { - "name": items[i][0], - "count": Number(items[i][1]), - "instanceName":instance, - "instanceID":instanceconfig.unique, - }; - } - } - })(i); - } - // request our items, one item at a time - for (let i = 0; i < Object.keys(preparedPackage).length; i++) { - // console.log(preparedPackage[Object.keys(preparedPackage)[i]]); - needle.post(config.masterIP + ":" + config.masterPort + '/api/remove', preparedPackage[Object.keys(preparedPackage)[i]], needleOptionsWithTokenAuthHeader, function (err, response, body) { - if (response && response.body && typeof response.body == "object") { - // buffer confirmed orders - confirmedOrders[confirmedOrders.length] = {name:response.body.name,count:response.body.count} - if(config.logItemTransfers){ - console.log(`Imported ${response.body.count} ${response.body.name} from master`); - } - } - }); - } - // if we got some confirmed orders - // console.log("Importing " + confirmedOrders.length + " items! " + JSON.stringify(confirmedOrders)); - //if (!(confirmedOrders.length>0)){return;} - let cmd="local t={"; - for(let i=0;i320) // Factorio max packet size is 508 - { - messageInterface("/silent-command remote.call('clusterio', 'runcode', '"+cmd.slice(0, -1)+"}"+ " for k, item in pairs(t) do GiveItemsToStorage(k, item) end')"); - cmd="local t={"; - } - } - if (!(cmd==="local t={")){ - messageInterface("/silent-command remote.call('clusterio', 'runcode', '"+cmd.slice(0, -1)+"}"+ " for k, item in pairs(t) do GiveItemsToStorage(k, item) end')"); - } - confirmedOrders=[]; - } - }, 1000); - // COMBINATOR SIGNALS --------------------------------------------------------- - // get inventory from Master and RCON it to our slave - setInterval(function () { - needle.get(config.masterIP + ":" + config.masterPort + '/api/inventory', function (err, response, body) { - if(err){ - console.error("Unable to get JSON master/api/inventory, master might be unaccessible"); - } else if (response && response.body) { - // Take the inventory we (hopefully) got and turn it into the format LUA accepts - if(Buffer.isBuffer(response.body)) {console.log(response.body.toString("utf-8")); throw new Error();} // We are probably contacting the wrong webserver - try { - var inventory = JSON.parse(response.body); - var inventoryFrame = {}; - for (let i = 0; i < inventory.length; i++) { - inventoryFrame[inventory[i].name] = Number(inventory[i].count); - if(inventoryFrame[inventory[i].name] >= Math.pow(2, 31)){ - inventoryFrame[inventory[i].name] = Math.pow(2, 30); // set it waaay lower, 31 -1 would probably suffice - } - } - inventoryFrame["signal-unixtime"] = Math.floor(Date.now()/1000); - // console.log("RCONing inventory! " + JSON.stringify(inventoryFrame)); - let first = true; - let cmd="local s={"; - for (let key in inventoryFrame) - { - cmd+='["'+key+'"]='+inventoryFrame[key]+","; - if(first && cmd.length>300 || !first && cmd.length > 320) // Factorio max packet size is 508 - { - messageInterface("/silent-command remote.call('clusterio', 'runcode', '"+(first ? 'global.ticksSinceMasterPinged=0 ':'')+cmd.slice(0, -1)+"}"+ " for name,count in pairs(s) do global.invdata[name]=count end')"); - cmd="local s={"; - first = false; - } - } - if (!(cmd==="local s={")){ - messageInterface("/silent-command remote.call('clusterio', 'runcode', '"+(first ? 'global.ticksSinceMasterPinged=0 ':'')+cmd.slice(0, -1)+"}"+ " for name,count in pairs(s) do global.invdata[name]=count end')"); - } - messageInterface("/silent-command remote.call('clusterio', 'runcode', 'UpdateInvCombinators()')"); - } catch (e){ - console.log(e); - } - } - }); - }, 1550); - // Make sure world has its worldID - setTimeout(function(){ - messageInterface("/silent-command remote.call('clusterio','setWorldID',"+instanceconfig.unique+")") - }, 20000); - /* REMOTE SIGNALLING - * send any signals the slave has been told to send - * Fetch combinator signals from the server - */ - socket.on("processCombinatorSignal", circuitFrameWithMeta => { - if(circuitFrameWithMeta && typeof circuitFrameWithMeta == "object" && circuitFrameWithMeta.frame && Array.isArray(circuitFrameWithMeta.frame)){ - messageInterface("/silent-command remote.call('clusterio', 'receiveFrame', '"+JSON.stringify(circuitFrameWithMeta.frame)+"')"); - } - }); - // get outbound frames from file and send to master - // get array of lines in file, each line should correspond to a JSON encoded frame - let signals = fs.readFileSync(instancedirectory + "/script-output/txbuffer.txt", "utf8").split("\n"); - // if we actually got anything from the file, proceed and reset file - let readingTxBufferSoon = false; - let txBufferClearCounter = 0; - fs.watch(instancedirectory + "/script-output/txbuffer.txt", "utf-8", (eventType, filename) => { - if(!readingTxBufferSoon){ // use a 100ms delay to avoid messing with rapid sequential writes from factorio (I think that might be a problem maybe?) - readingTxBufferSoon = true; - setTimeout(()=>{ - txBufferClearCounter++; - fs.readFile(instancedirectory + "/script-output/txbuffer.txt", "utf-8", (err, signals) => { - signals = signals.split("\n"); - if (signals[0]) { - //if(txBufferClearCounter > 500){ - fs.writeFileSync(instancedirectory + "/script-output/txbuffer.txt", ""); - // txBufferClearCounter = 0; - //} - - // loop through all our frames - for (let i = 0; i < signals.length; i++) { - if (signals[i] && objectOps.isJSON(signals[i])) { - // signals[i] is a JSON array called a "frame" of signals. We timestamp it for storage on master - // then we unpack and RCON in this.frame to the game later. - let framepart = JSON.parse(signals[i]); - let doneframe = { - time: Date.now(), - frame: framepart, // thats our array of objects(single signals); - } - // send to master using socket.io, opened at the top of instanceManagement() - socket.emit("combinatorSignal", doneframe); - } else { - // console.log("Invalid jsony: "+typeof signals[i]) - } - } - } - }); - readingTxBufferSoon = false; - },100); - } - }); } // END OF INSTANCE START --------------------------------------------------------------------- // string, function diff --git a/sharedPlugins/clusterioMod/config.js b/sharedPlugins/clusterioMod/config.js new file mode 100644 index 00000000..01c46abf --- /dev/null +++ b/sharedPlugins/clusterioMod/config.js @@ -0,0 +1,11 @@ +/* + Clusterio plugin to allow for chat between instances. +*/ +module.exports = { + // Name of package. For display somewhere I guess. + name: "clusterioMod", + version: "1.0.0", + binary: "nodePackage", + description: "Server side handling of functionality in the clusterio lua mod", + masterPlugin: "masterPlugin.js", +} diff --git a/sharedPlugins/clusterioMod/index.js b/sharedPlugins/clusterioMod/index.js new file mode 100644 index 00000000..fec5da1a --- /dev/null +++ b/sharedPlugins/clusterioMod/index.js @@ -0,0 +1,395 @@ +const needle = require("needle"); +const fs = require("fs-extra"); + +const objectOps = require("./../../lib/objectOps.js"); +const fileOps = require("_app/fileOps"); +const stringUtils = require("./../../lib/stringUtils.js"); + +const pluginConfig = require("./config"); +const COMPRESS_LUA = false; + +module.exports = class remoteCommands { + constructor(mergedConfig, messageInterface, extras){ + this.messageInterface = messageInterface; + this.config = mergedConfig; + this.socket = extras.socket; + this.socket.on("hello", () => { + + }); + const that = this; + const instance = process.argv[3]; + const instancedirectory = this.config.instanceDirectory + '/' + instance; + + const needleOptionsWithTokenAuthHeader = { + headers: { + 'x-access-token': this.config.masterAuthToken + }, + }; + + (async ()=>{ + let hotpatchInstallStatus = await this.checkHotpatchInstallation(); + this.messageInterface("Hotpach installation status: "+hotpatchInstallStatus); + if(hotpatchInstallStatus){ + // let mainCode = await this.getSafeLua("sharedPlugins/playerManager/lua/playerTracking.lua"); + // if(mainCode) var returnValue = await messageInterface("/silent-command remote.call('hotpatch', 'update', '"+pluginConfig.name+"', '"+pluginConfig.version+"', '"+mainCode+"')"); + } + })().catch(e => console.log(e)); + + // flow/production statistics ------------------------------------------------------------ + var oldFlowStats = false; + var oldTimestamp; + var oldFlowStats; + setInterval(function(){ + fs.readFile(instancedirectory + "/script-output/flows.txt", {encoding: "utf8"}, function(err, data) { + if(!err && data) { + let timestamp = Date.now(); + data = data.split("\n"); + let flowStats = []; + for(let i = 0; i < data.length; i++) { + // try catch to remove any invalid json + try{ + flowStats[flowStats.length] = JSON.parse(data[i]); + } catch (e) { + // console.log(" invalid json: " + i); + // some lines of JSON are invalid but don't worry, we just filter em out + } + } + // fluids + let flowStat1 = flowStats[flowStats.length-1].flows.player.input_counts + // items + let flowStat2 = flowStats[flowStats.length-2].flows.player.input_counts + // merge fluid and item flows + let totalFlows = {}; + for(let key in flowStat1) totalFlows[key] = flowStat1[key]; + for(let key in flowStat2) totalFlows[key] = flowStat2[key]; + if(oldFlowStats && totalFlows && oldTimestamp) { + let payload = objectOps.deepclone(totalFlows); + // change from total reported to per time unit + for(let key in oldFlowStats) { + // get production per minute + payload[key] = Math.floor((payload[key] - oldFlowStats[key])/(timestamp - oldTimestamp)*60000); + if(payload[key] < 0) { + payload[key] = 0; + } + } + for(let key in payload) { + if(payload[key] == '0') { + delete payload[key]; + } + } + console.log("Recorded flows, copper plate since last time: " + payload["copper-plate"]); + needle.post(that.config.masterIP + ":" + that.config.masterPort + '/api/logStats', {timestamp: timestamp, instanceID: that.config.unique, data: payload}, needleOptionsWithTokenAuthHeader, function (err, response, body) { + // we did it, keep going + }); + } + oldTimestamp = timestamp; + oldFlowStats = totalFlows; + fs.writeFileSync(instancedirectory + "/script-output/flows.txt", ""); + } + }); + // we don't need to update stats quickly as that could be expensive + }, 60000*5); + + // 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 + let 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 (let i = 0; i < items.length; i++) { + if (items[i]) { + let 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(that.config.masterIP + ":" + that.config.masterPort + '/api/place', { + name: g[0], + count: g[1], + instanceName: instance, // name of instance + instanceID: that.config.unique, // a hash computed from the randomly generated rcon password + }, needleOptionsWithTokenAuthHeader, function (err, resp, body) { + if(body == "failure") console.error("#### Export failed! Lost: "+g[1]+" "+g[0]); + if(that.config.logItemTransfers){ + if(body == "success") console.log(`Exported ${g[1]} ${g[0]} to master`); + } + }); + } + } + }); + + // request items -------------------------------------------------------------- + setInterval(function () { + // get array of lines in file + let 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 transfer friendly format + var preparedPackage = {}; + for (let 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 we have buffered some already, sum the new items + 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]), + "instanceName":instance, + "instanceID":that.config.unique, + }; + // else just add em in without summing + } else if (typeof Number(items[i][1]) == "number") { + preparedPackage[items[i][0]] = { + "name": items[i][0], + "count": Number(items[i][1]), + "instanceName":instance, + "instanceID":that.config.unique, + }; + } + // this condition will NEVER be triggered but we know how that goes + } else if (typeof Number(items[i][1]) == "number") { + preparedPackage[items[i][0]] = { + "name": items[i][0], + "count": Number(items[i][1]), + "instanceName":instance, + "instanceID":that.config.unique, + }; + } + } + })(i); + } + // request our items, one item at a time + for (let i = 0; i < Object.keys(preparedPackage).length; i++) { + // console.log(preparedPackage[Object.keys(preparedPackage)[i]]); + needle.post(that.config.masterIP + ":" + that.config.masterPort + '/api/remove', preparedPackage[Object.keys(preparedPackage)[i]], needleOptionsWithTokenAuthHeader, function (err, response, body) { + if (response && response.body && typeof response.body == "object") { + // buffer confirmed orders + confirmedOrders[confirmedOrders.length] = {name:response.body.name,count:response.body.count} + if(that.config.logItemTransfers){ + console.log(`Imported ${response.body.count} ${response.body.name} from master`); + } + } + }); + } + // if we got some confirmed orders + // console.log("Importing " + confirmedOrders.length + " items! " + JSON.stringify(confirmedOrders)); + //if (!(confirmedOrders.length>0)){return;} + let cmd="local t={"; + for(let i=0;i320) // Factorio max packet size is 508 + { + that.messageInterface("/silent-command remote.call('clusterio', 'runcode', '"+cmd.slice(0, -1)+"}"+ " for k, item in pairs(t) do GiveItemsToStorage(k, item) end')"); + cmd="local t={"; + } + } + if (!(cmd==="local t={")){ + that.messageInterface("/silent-command remote.call('clusterio', 'runcode', '"+cmd.slice(0, -1)+"}"+ " for k, item in pairs(t) do GiveItemsToStorage(k, item) end')"); + } + confirmedOrders=[]; + } + }, 1000); + // COMBINATOR SIGNALS --------------------------------------------------------- + // get inventory from Master and RCON it to our slave + setInterval(() => { + needle.get(that.config.masterIP + ":" + that.config.masterPort + '/api/inventory', function (err, response, body) { + if(err){ + console.error("Unable to get JSON master/api/inventory, master might be unaccessible"); + } else if (response && response.body) { + // Take the inventory we (hopefully) got and turn it into the format LUA accepts + if(Buffer.isBuffer(response.body)) {console.log(response.body.toString("utf-8")); throw new Error();} // We are probably contacting the wrong webserver + try { + var inventory = JSON.parse(response.body); + var inventoryFrame = {}; + for (let i = 0; i < inventory.length; i++) { + inventoryFrame[inventory[i].name] = Number(inventory[i].count); + if(inventoryFrame[inventory[i].name] >= Math.pow(2, 31)){ + inventoryFrame[inventory[i].name] = Math.pow(2, 30); // set it waaay lower, 31 -1 would probably suffice + } + } + inventoryFrame["signal-unixtime"] = Math.floor(Date.now()/1000); + // console.log("RCONing inventory! " + JSON.stringify(inventoryFrame)); + let first = true; + let cmd="local s={"; + for (let key in inventoryFrame) + { + cmd+='["'+key+'"]='+inventoryFrame[key]+","; + if(first && cmd.length>300 || !first && cmd.length > 320) // Factorio max packet size is 508 + { + that.messageInterface("/silent-command remote.call('clusterio', 'runcode', '"+(first ? 'global.ticksSinceMasterPinged=0 ':'')+cmd.slice(0, -1)+"}"+ " for name,count in pairs(s) do global.invdata[name]=count end')"); + cmd="local s={"; + first = false; + } + } + if (!(cmd==="local s={")){ + that.messageInterface("/silent-command remote.call('clusterio', 'runcode', '"+(first ? 'global.ticksSinceMasterPinged=0 ':'')+cmd.slice(0, -1)+"}"+ " for name,count in pairs(s) do global.invdata[name]=count end')"); + } + that.messageInterface("/silent-command remote.call('clusterio', 'runcode', 'UpdateInvCombinators()')"); + } catch (e){ + console.log(e); + } + } + }); + }, 1550); + // Make sure world has its worldID + setTimeout(function(){ + messageInterface("/silent-command remote.call('clusterio','setWorldID',"+that.config.unique+")") + }, 20000); + /* REMOTE SIGNALLING + * send any signals the slave has been told to send + * Fetch combinator signals from the server + */ + this.socket.on("processCombinatorSignal", circuitFrameWithMeta => { + if(circuitFrameWithMeta && typeof circuitFrameWithMeta == "object" && circuitFrameWithMeta.frame && Array.isArray(circuitFrameWithMeta.frame)){ + messageInterface("/silent-command remote.call('clusterio', 'receiveFrame', '"+JSON.stringify(circuitFrameWithMeta.frame)+"')"); + } + }); + // get outbound frames from file and send to master + // get array of lines in file, each line should correspond to a JSON encoded frame + let signals = fs.readFileSync(instancedirectory + "/script-output/txbuffer.txt", "utf8").split("\n"); + // if we actually got anything from the file, proceed and reset file + let readingTxBufferSoon = false; + let txBufferClearCounter = 0; + fs.watch(instancedirectory + "/script-output/txbuffer.txt", "utf-8", (eventType, filename) => { + if(!readingTxBufferSoon){ // use a 100ms delay to avoid messing with rapid sequential writes from factorio (I think that might be a problem maybe?) + readingTxBufferSoon = true; + setTimeout(()=>{ + txBufferClearCounter++; + fs.readFile(instancedirectory + "/script-output/txbuffer.txt", "utf-8", (err, signals) => { + signals = signals.split("\n"); + if (signals[0]) { + //if(txBufferClearCounter > 500){ + fs.writeFileSync(instancedirectory + "/script-output/txbuffer.txt", ""); + // txBufferClearCounter = 0; + //} + + // loop through all our frames + for (let i = 0; i < signals.length; i++) { + if (signals[i] && objectOps.isJSON(signals[i])) { + // signals[i] is a JSON array called a "frame" of signals. We timestamp it for storage on master + // then we unpack and RCON in this.frame to the game later. + let framepart = JSON.parse(signals[i]); + let doneframe = { + time: Date.now(), + frame: framepart, // thats our array of objects(single signals); + } + // send to master using socket.io, opened at the top of instanceManagement() + socket.emit("combinatorSignal", doneframe); + } else { + // console.log("Invalid jsony: "+typeof signals[i]) + } + } + } + }); + readingTxBufferSoon = false; + },100); + } + }); + } + async getCommand(file){ + this.commandCache = this.commandCache || {}; + if(!this.commandCache[file]){ + try{ + let command = (await fs.readFile(file)).toString(); + this.commandCache[file] = command.replace(/(\r\n\t|\n|\r\t)/gm, " "); // remove newlines + return this.commandCache[file]; + } catch(e){ + console.log("Unable to get command from file!"); + console.log(e) + } + } else if(typeof this.commandCache[file] == "string"){ + return this.commandCache[file]; + } else { + throw new Error("Command not found"); + } + } + async factorioOutput(data){ + + } + getInstanceName(instanceID){ + return new Promise((resolve, reject) => { + let instance = this.instances[instanceID]; + if(!instance){ + needle.get(this.config.masterIP+":"+this.config.masterPort+ '/api/slaves', (err, response) => { + if(err || response.statusCode != 200) { + console.log("Unable to get JSON master/api/slaves, master might be unaccessible"); + } else if (response && response.body) { + if(Buffer.isBuffer(response.body)) {console.log(response.body.toString("utf-8")); throw new Error();} + try { + for (let index in response.body) + this.instances[index] = response.body[index].instanceName; + } catch (e){ + console.log(e); + return null; + } + instance = this.instances[instanceID] + if (!instance) instance = instanceID; //somehow the master doesn't know the instance + resolve(instance); + } + }); + } else { + resolve(instance); + } + }); + } + async getSafeLua(filePath){ + return new Promise((resolve, reject) => { + fs.readFile(filePath, "utf8", (err, contents) => { + if(err){ + reject(err); + } else { + // split content into lines + contents = contents.split(/\r?\n/); + + // join those lines after making them safe again + contents = contents.reduce((acc, val) => { + val = val.replace(/\\/g ,'\\\\'); + // remove leading and trailing spaces + val = val.trim(); + // escape single quotes + val = val.replace(/'/g ,'\\\''); + + // remove single line comments + let singleLineCommentPosition = val.indexOf("--"); + let multiLineCommentPosition = val.indexOf("--[["); + + if(multiLineCommentPosition === -1 && singleLineCommentPosition !== -1) { + val = val.substr(0, singleLineCommentPosition); + } + + return acc + val + '\\n'; + }, ""); // need the "" or it will not process the first row, potentially leaving a single line comment in that disables the whole code + + // console.log(contents); + + // this takes about 46 ms to minify train_stop_tracking.lua in my tests on an i3 + if(COMPRESS_LUA) contents = require("luamin").minify(contents); + + resolve(contents); + } + }); + }); + } + async checkHotpatchInstallation(){ + let yn = await this.messageInterface("/silent-command if remote.interfaces['hotpatch'] then rcon.print('true') else rcon.print('false') end"); + yn = yn.replace(/(\r\n\t|\n|\r\t)/gm, ""); + if(yn == "true"){ + return true; + } else if(yn == "false"){ + return false; + } + } +} +async function sleep(s){ + return new Promise((resolve, reject) => { + setTimeout(resolve, s*1000); + }); +} diff --git a/sharedPlugins/clusterioMod/masterPlugin.js b/sharedPlugins/clusterioMod/masterPlugin.js new file mode 100644 index 00000000..9123a1c5 --- /dev/null +++ b/sharedPlugins/clusterioMod/masterPlugin.js @@ -0,0 +1,85 @@ +const fs = require("fs-extra"); +const path = require("path"); +const Express = require("express"); +const ejs = require("ejs"); + +const pmSockets = []; + +class masterPlugin { + constructor({config, pluginConfig, pluginPath, socketio, express}){ + 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 = {}; + } + 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); + } + } +} diff --git a/static/head.html b/static/head.html index f2a4b8e0..5b33e429 100644 --- a/static/head.html +++ b/static/head.html @@ -7,4 +7,4 @@ - \ No newline at end of file + \ No newline at end of file