mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-12 15:04:52 +09:00
[big change] Moved core clusterio Lua mod functionality into its own plugin
This commit is contained in:
1 parent
f7f2ce6897
commit
4fed43bad6
6 files changed
+494
-258
No files matched your search
@@ -50,5 +50,6 @@ sharedPlugins/*
|
||||
!sharedPlugins/remoteCommands/
|
||||
!sharedPlugins/researchSync/
|
||||
!sharedPlugins/globalChat/
|
||||
!sharedPlugins/clusterioMod/
|
||||
|
||||
\.cache-require-paths\.json
|
||||
@@ -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;i<confirmedOrders.length;i++)
|
||||
{
|
||||
cmd+='["'+confirmedOrders[i].name+'"]='+confirmedOrders[i].count+',';
|
||||
if(cmd.length>320) // 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
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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;i<confirmedOrders.length;i++)
|
||||
{
|
||||
cmd+='["'+confirmedOrders[i].name+'"]='+confirmedOrders[i].count+',';
|
||||
if(cmd.length>320) // 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);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -7,4 +7,4 @@
|
||||
<script src="/external/mousetrap.min.js"></script>
|
||||
<script src="/main.js"></script>
|
||||
<link href="/external/css/Roboto.css" rel="stylesheet">
|
||||
<link rel="icon" href="favicon.ico" type="image/x-icon" />
|
||||
<link rel="icon" href="/favicon.ico" type="image/x-icon" />
|
||||
Reference in new issue
Block a user