diff --git a/.gitignore b/.gitignore index 9b288416..b31280a2 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,11 @@ Factorio* /instances /sharedMods /database -/sharedPlugins database/items.db -config.json \ No newline at end of file +config.json + +# weird pattern to ignore sharedPlugins except not ignore some core modules +!sharedPlugins/ +sharedPlugins/* +!sharedPlugins/inventoryImports/ +!sharedPlugins/UPSdisplay/ \ No newline at end of file diff --git a/client.js b/client.js index 6570e837..cf1bde39 100644 --- a/client.js +++ b/client.js @@ -66,9 +66,9 @@ function messageInterface(command, callback) { client.exec(command, callback); } catch (err) { console.log(err); - if(typeof callback == "function"){ - callback(err); - } + } + if(typeof callback == "function"){ + callback(); } } } @@ -111,6 +111,10 @@ if (!command || command == "help" || command == "--help") { console.log("Downloaded "+name); }); } +} else if (command == "start" && instance === undefined) { + console.log("ERROR: No instanceName provided!"); + console.log("Usage: node client.js start [instanceName]"); + process.exit(0); } else if (command == "start" && typeof instance == "string" && instance != "/" && !fs.existsSync(instancedirectory)) { // if instance does not exist, create it console.log("Creating instance..."); @@ -305,7 +309,7 @@ function instanceManagement() { // load plugins and execute onLoad event let pluginDirectories = fileOps.getDirectoriesSync("./sharedPlugins/"); let plugins = []; - for(i=0; i { + var chunk = process.stdin.read(); + // console.log(chunk) + if (chunk !== null && isNaN(chunk) && config && isJson(chunk) && !JSON.parse(chunk).factorioPort){ + + let inventory = JSON.parse(chunk) + // process inventory of players and ask master for more stuff + functions.handleInventory(inventory, config, console.log); + } else if(isJson(chunk) && JSON.parse(chunk).factorioPort){ + config = JSON.parse(chunk); + } +}); +process.stdin.on('end', () => { + process.stdout.write('end'); +}); + +// poll inventories +setInterval(function(){ + // console.log("Polling inventories\n") + console.log(functions.pollInventories(pluginConfig.scriptOutputFileSubscription)); +}, 10000); diff --git a/sharedPlugins/inventoryImports/interfacingFunctions.js b/sharedPlugins/inventoryImports/interfacingFunctions.js new file mode 100644 index 00000000..60898b98 --- /dev/null +++ b/sharedPlugins/inventoryImports/interfacingFunctions.js @@ -0,0 +1,155 @@ +const objectOps = require("../../lib/objectOps.js"); +const needle = require("needle"); +const pluginConfig = require("./config") +const isJson = require("./isJson") + +// returns JS object, throws if there are syntax errors in input string +function parseJsString(string){ + // eval because my lua is writing a JS object instead of traditional JSON + // this was done because I hate escaping strings and there are more quotes + // in JSON than JS objects. + + // since eval is unsafe, maybe use https://www.npmjs.com/package/eval-sanitizer + if(string.includes("require") || string.includes(";") || string.includes("eval")){ + throw "parseJsString might have gotten something that could be a xss attempt"; + } + let inventory = ""; + eval("inventory = " + string); + /*console.log(string) + console.log(inventory)*/ + return inventory; +} +function handleInventory(json, config, callback){ + /* + json is one of these two samples + {"players":{"1":{"inventory":{"stone":12,"iron-ore":120,"raw-wood":10,"iron-plate":8,"steel-plate":480},"requestSlots":{}}}} + {"exports":{"1":[{"name":"iron-ore","count":0},{"name":"steel-plate","count":0}]}} + + we want to seperate players, parse their json and compare to request slots + then ask master for the difference + we must then prepare it all and inject it into players inventories with lua before + the next json report arrives to avoid duplicating orders + */ + if(json && typeof json != "string" && typeof json == "object"){ + let exports = {}; + + // this is an inventory report, check whats missing and request from master + // {"players":{"1":{"inventory":{"stone":12,"iron-ore":120,"raw-wood":10,"iron-plate":8,"steel-plate":480},"requestSlots":{}}}} + if(json.players){ + let playerNames = Object.keys(json.players); + for(let i = 0; i < playerNames.length; i++) { + // haha, this turned into bit of a mess didn't it? + let playerInv = json.players[playerNames[i]]; + // create a copy of player.requestSlots + let toRequest = objectOps.deepclone(playerInv.requestSlots); + let inventory = playerInv.inventory; + let itemsToConfirm = Object.keys(toRequest).length; + // create a place to store the object we have confirmed for importing + let confirmedItems = {}; + let collectStacks = function(stack){ + itemsToConfirm--; + if(stack){ + // because of the way we collect data in a keyed JSON object, this first part of the IF cannot happen. + /* istanbul ignore next */ + if(confirmedItems[stack.name] && confirmedItems[stack.name] > 0){ + confirmedItems[stack.name] += stack.count; + } else { + confirmedItems[stack.name] = stack.count; + } + } + if(itemsToConfirm == 0 && Object.keys(confirmedItems).length > 0){ + // return LUA + callback(insertItemsFromObject(confirmedItems, playerNames[i])); + } + } + // subtract items already in inventory + Object.keys(playerInv.requestSlots).forEach(function(key){ + // if this item in inventory is also in a request slot, subtract invCount from requestCount + if(inventory[key]){ + toRequest[key] -= inventory[key]; + } + // if the difference is more than 0, request item from master + if(toRequest[key] > 0){ + let request = { + name:key, + count:toRequest[key], + } + //console.log("Requesting: "+JSON.stringify(request)); + // request the difference from master + needle.post(config.masterIP + ":" + config.masterPort + '/api/remove', request, {json:true}, function (err, response, body) { + if (!err && response && response.body && (typeof response.body == "object" || isJson(response.body))) { + // confirmed orders, already removed from master inventory + //response.body.name: response.body.count + if(typeof response.body == "string") response.body = JSON.parse(response.body); + let stack = {name:response.body.name,count:Number(response.body.count)} + collectStacks(stack); + } else { + collectStacks(false); + } + }); + } else { + collectStacks(false); + } + }); + } + } + // {"exports":{"1":[{"name":"iron-ore","count":0},{"name":"steel-plate","count":0}]}} + // this is a report of stuff we didn't have space for, send back to master + if(json.exports){ + // loop over players (normally only one, but whatever) + Object.keys(json.exports).forEach(function(playerName){ + // loop over simpleItemStacks to export + let stackArray = json.exports[playerName]; + for(let i = 0; i < stackArray.length; i++){ + if(stackArray[i].count > 0){ + // console.log("Returning overflow: " + JSON.stringify(stackArray[i])); + needle.post(config.masterIP + ":" + config.masterPort + '/api/place', stackArray[i], function (err, resp, body) { + // console.log(body); + }); + } + } + }); + } + } else return false; // inventory is falsey or a string +} +function insertItemsFromObject(confirmedItems, playerName){ + // expeced input + // {["iron-plate"]:100, coal:20} + // make lua table + // we gotta construct a string because I can't find any nice library for this + if(playerName && confirmedItems){ + if(typeof playerName != "string") { + throw "playerName is: '"+typeof playerName+"' instead of string!"; + } + let itemTable = "{"; + Object.keys(confirmedItems).forEach(function(name){ + let count = confirmedItems[name]; + itemTable += '["'+name+'"]='+count+','; + }); + itemTable += "}"; + let outputFile = pluginConfig.scriptOutputFileSubscription; + // a="'+players[i]+'" + // b='+itemTable+' + return '/silent-command local a="'+playerName+'"local b='+itemTable+'local c={}if game.players[a]and game.players[a].connected then local d='+"'"+'{"exports":{"'+"'"+'..a..'+"'"+'":['+"'"+'local e=false;for f,g in pairs(b)do local h=game.players[a].insert{name=f,count=g}if e then d=d..'+"'"+','+"'"+'else e=true end;d=d..'+"'"+'{"name":"'+"'"+'..f..'+"'"+'","count":'+"'"+'..g-h..'+"'"+'}'+"'"+'end;d=d..'+"'"+']}}'+"'"+'game.write_file("'+outputFile+'",d)end'; + } +} +function pollInventories(outputFile){ + // console.log("/silent-command "+'local a=game.players["Danielv123"].get_inventory(defines.inventory.player_main).get_contents()local b=game.players["Danielv123"].get_quickbar().get_contents()local c={}local d="{inventory:{"for e,f in pairs(a)do d=d.."['+"'"+'"..e.."'+"'"+']:"..f..","end;for e,f in pairs(b)do d=d.."['+"'"+'"..e.."'+"'"+']:"..f..","end;d=d.."},requestSlots:{"for e,f in pairs(c)do d=d.."['+"'"+'"..e.."'+"'"+']:"..f..","end;d=d.."}}"game.write_file("'+pluginConfig.scriptOutputFileSubscription+'",d)'); + // console.log("Running inventory LUA") + //console.log("/silent-command "+'local a=game.players["Danielv123"].get_inventory(defines.inventory.player_main).get_contents()local b=game.players["Danielv123"].get_quickbar().get_contents()local c={}for d=1,game.players["Danielv123"].force.character_logistic_slot_count do c[d]=game.players["Danielv123"].character.get_request_slot(d)end;local e="{inventory:{"for f,g in pairs(a)do e=e.."['+"'"+'"..f.."'+"'"+']:"..g..","end;for f,g in pairs(b)do e=e.."['+"'"+'"..f.."'+"'"+']:"..g..","end;e=e.."},requestSlots:{"for f,g in pairs(c)do e=e.."['+"'"+'"..g["name"].."'+"'"+']:"..g["count"]..", "end;e=e.."}}"game.write_file("'+pluginConfig.scriptOutputFileSubscription+'",e)') + // console.log("/silent-command "+'local a="{"for b,c in pairs(game.players)do local d=game.players[b].get_inventory(defines.inventory.player_main).get_contents()local e=game.players[b].get_quickbar().get_contents()local f={}for g=1,game.players[b].force.character_logistic_slot_count do f[g]=game.players[b].character.get_request_slot(g)end;a=a..b..":{inventory:{"for h,i in pairs(d)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;for h,i in pairs(e)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;a=a.."},requestSlots:{"for h,i in pairs(f)do a=a.."['+"'"+'"..i["name"].."'+"'"+']:"..i["count"]..", "end;a=a.."}},"end;a=a.."}"game.write_file("'+pluginConfig.scriptOutputFileSubscription+'",a)') + //return "/silent-command "+'local a="{"for b,c in pairs(game.players)do if c.connected then local d=game.players[b].get_inventory(defines.inventory.player_main).get_contents()local e=game.players[b].get_quickbar().get_contents()local f={}for g=1,game.players[b].force.character_logistic_slot_count do f[g]=game.players[b].character.get_request_slot(g)end;a=a..b..":{inventory:{"for h,i in pairs(d)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;for h,i in pairs(e)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;a=a.."},requestSlots:{"for h,i in pairs(f)do a=a.."['+"'"+'"..i["name"].."'+"'"+']:"..i["count"]..", "end;a=a.."}},"end end;a=a.."}"game.write_file("'+outputFile+'",a)' + return '/silent-command '+'local a='+"'"+'{"players":{'+"'"+'local b=false;for c,d in pairs(game.players)do if d.connected then if b then a=a..'+"'"+','+"'"+'else b=true end;local e=game.players[c].get_inventory(defines.inventory.player_main).get_contents()local f=game.players[c].get_quickbar().get_contents()local g={}for h=1,game.players[c].force.character_logistic_slot_count do g[h]=game.players[c].character.get_request_slot(h)end;a=a..'+"'"+'"'+"'"+'..c..'+"'"+'":{"inventory":{'+"'"+'local i=false;for j,k in pairs(e)do if i then a=a..'+"'"+','+"'"+'else i=true end;a=a..'+"'"+'"'+"'"+'..j..'+"'"+'":'+"'"+'..k end;for j,k in pairs(f)do if i then a=a..'+"'"+','+"'"+'else i=true end;a=a..'+"'"+'"'+"'"+'..j..'+"'"+'":'+"'"+'..k end;a=a..'+"'"+'},"requestSlots":{'+"'"+'i=false;for j,k in pairs(g)do if i then a=a..'+"'"+','+"'"+'else i=true end;a=a..'+"'"+'"'+"'"+'..k['+"'"+'name'+"'"+']..'+"'"+'":'+"'"+'..k['+"'"+'count'+"'"+']end;a=a..'+"'"+'}}'+"'"+'end end;a=a.."}}"game.write_file("'+outputFile+'",a)' + /* + needle.post(config.masterIP+':'+config.masterPort+'/api/editSlaveMeta', {slaveID: config.unique, password: config.clientPassword, meta: {UPS:UPS}}, function(err, resp) { + // success? + }); + */ +} + +module.exports = { + handleInventory: handleInventory, + parseJsString: parseJsString, + pollInventories: pollInventories, + insertItemsFromObject: insertItemsFromObject, +} \ No newline at end of file diff --git a/sharedPlugins/inventoryImports/interfacingFunctions.spec.js b/sharedPlugins/inventoryImports/interfacingFunctions.spec.js new file mode 100644 index 00000000..9b1639a3 --- /dev/null +++ b/sharedPlugins/inventoryImports/interfacingFunctions.spec.js @@ -0,0 +1,178 @@ +const assert = require("assert"); +const child_process = require("child_process"); +const functions = require("./interfacingFunctions"); +const luamin = require("luamin"); +const isFactorioCommand = require("./isFactorioCommand"); +const nock = require("nock"); + +// sinon does weird prototype manipulation and so doen't need to be saved to anything. +require("mocha-sinon"); +const sinon = require("sinon"); + +describe("inventoryImports/interfacingFunctions.js", function(){ + describe("handleInventory(inventory, config)", function(){ + let config = { + masterIP:"localhost", + masterPort:8080, + } + it("returns false if given invalid input", function(){ + assert(functions.handleInventory("this is a string") === false, "when given strings it should return false and do nothing"); + }); + it("Sends post requests and outputs factorio commands", function(done){ + let inventory = { + "players":{ + "Danielv123":{ + "inventory":{ + "stone":12, + "iron-ore":120, + "raw-wood":10, + "iron-plate":8, + "steel-plate":480 + }, + "requestSlots":{ + "iron-plate":100, + "steel-plate":480, + "raw-wood":20, + "bullshit":0, + } + } + } + } + var scope = nock('http://localhost:8080') + .post('/api/remove', function(body){ + // check that we are requesting the right things, () to convert to boolean + // 92 because 100 requested and 8 already in inventory + if(body.name == "iron-plate") assert(body.count == "92"); + // nock wants us to return true if this is the correct body and we should reply + // if we return false we won't send a reply. + return true; + }) + // nock destroys the endpoints after being hit once, this changes the number of hits required for destruction + .times(10) + // respond with JSON object instead of url query parameters + .reply(200,function(uri, req){ + return req + },{ + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }) + // nock logging statement .log((data) => console.log(data)); + + functions.handleInventory(inventory, config, callback); + function callback(x){ + assert(x.includes(92) && x.includes("iron-plate"), "it should have included the items it was going to import"); + assert(!x.includes("steel-plate"), "Inventory is already filled with steel so shouldn't be importing that"); + assert(isFactorioCommand(x), "it should have returned a valid factorio command"); + assert(x.includes('"Danielv123"'), "it should include the name of the player that will recieve the items"); + done(); + } + }); + it("posts leftovers to master", function(done){ + let inventory2 = { + "exports":{ + "1":[ + {"name":"iron-ore","count":1200}, + {"name":"steel-plate","count":1337}, + {"name":"copper-plate","count":0}, + ] + } + } + let timesRan = 0; + let callback = function(){ + timesRan++ + assert(timesRan <= 2, "placed more than 2 items, unexpeceted test result"); + if(timesRan == 2){ + done(); + } + } + var scope = nock('http://localhost:8080') + .post('/api/place', function(body){ + // check that we are requesting the right things + assert(body.name != "copper-plate", "copper plate should not be placed because it is 0"); + assert(body.count > 0, "no need for placing 0 things"); + assert(Number(body.count) != NaN, "count should be a number"); + callback(); + return true; + }) + // nock destroys the endpoints after being hit once, this changes the number of hits required for destruction + .times(10) + .reply(200,"affirmative") + // if it tries using the nonexisting callback (argv 3) it will throw. It shoudln't. + assert.doesNotThrow(function(){ + functions.handleInventory(inventory2, config); + }, "this here threw for some reason, remove assert.doesNotThrow for easier debugging of this issue"); + }); + }); + describe("pollInventories(outputfile)", function(){ + it("Returns a big factorio command as string", function(){ + let x = functions.pollInventories("output.txt"); + assert(typeof x == "string", "pollInventories() should always return a string"); + assert(isFactorioCommand(x), "This is not a command factorio will accept, please prepend /c"); + assert(x.length > 20, "LUA string seems to short, this ain't good"); + }); + it("Returns valid LUA", function(){ + let x = functions.pollInventories("output.txt"); + + // remove string added to make factorio interpret as command + x = x.replace("/silent-command ","").replace("/c ",""); + + let y = false; + // luamin throws when it recieves something that is invalid LUA + assert.doesNotThrow(function(){ + y = luamin.minify(x); + }, "Invalid LUA supplied"); + assert(typeof y == "string", "luamin does not return string???"); + assert(y.length > 20, "Minified LUA too short, something is up"); + }); + }); + describe("insertItemsFromObject(object)", function(){ + let testObject = {"copper-plate":100, "iron-ore":1337} + let playerName = "Danielv123" + it("Returns undefined if any of the parameters are missing", function(){ + let x = functions.insertItemsFromObject(); + assert(x === undefined); + + let y = functions.insertItemsFromObject(testObject); + assert(y === undefined); + + let z = functions.insertItemsFromObject(undefined, playerName); + assert(z === undefined); + }); + it("Throws if player name is not a string", function(){ + assert.throws(function(){ + let x = functions.insertItemsFromObject(testObject, testObject); + }, "playerName is: 'object' instead of string!"); + }); + it("Returns a big factorio command as string", function(){ + let x = functions.insertItemsFromObject(testObject, playerName); + + assert(typeof x == "string", "LUA returned should be a string"); + assert(x.length > 20, "LUA strings are quite long"); + assert(isFactorioCommand(x), "This is not a command factorio will accept, please prepend /c"); + }); + it("The returned string is a valid factorio command", function(){ + let x = functions.insertItemsFromObject(testObject, playerName); + + assert(isFactorioCommand(x) == true); + }); + }); + describe("parseJsString(string) => object", function(){ + it("converts a string with JS object notation to object", function(){ + let x = functions.parseJsString("{hello:'thisAString!', yes:function(){return 'JS string!'}, not:'json'}"); + assert(!!x == true, "Should under no circumstance return a falsey value, even !!{} is truthy"); + assert(typeof x == "object", "parseJsString should return an object in this case"); + assert(typeof x.yes == "function", "parseJsString is not JSON, so it should be able to contain functions"); + }); + it("*Tries* to avoid XSS attempts by throwing on certain symbols", function(){ + assert.throws(function(){ + let x = functions.parseJsString("{};throw 'fail'"); + }, function(err) { + let error = err+"" + /* istanbul ignore else */ + if(error.includes('parseJsString might have gotten something that could be a xss attempt')){ + return true; + } + }, "should throw when ; are passed because those are the simplest form of xss"); + }); + }); +}); \ No newline at end of file diff --git a/sharedPlugins/inventoryImports/isFactorioCommand.js b/sharedPlugins/inventoryImports/isFactorioCommand.js new file mode 100644 index 00000000..239bbc59 --- /dev/null +++ b/sharedPlugins/inventoryImports/isFactorioCommand.js @@ -0,0 +1,23 @@ +const luamin = require("luamin"); +function isFactorioCommand(command){ + if(typeof command != "string"){ + return false; + } else if(!command.includes("/silent-command ") && !command.includes("/c ")){ + return false; + } else if(command[0] != "/"){ + return false; + } else { + // check that what follows /c is valid LUA + let x = command.replace("/silent-command ", "").replace("/c ",""); + let y + try{ + y = luamin.minify(x); + } catch (e) { + return false; + } + + // by this point it is surely a LUA command + return true; + } +} +module.exports = isFactorioCommand; \ No newline at end of file diff --git a/sharedPlugins/inventoryImports/isFactorioCommand.spec.js b/sharedPlugins/inventoryImports/isFactorioCommand.spec.js new file mode 100644 index 00000000..bc3ac232 --- /dev/null +++ b/sharedPlugins/inventoryImports/isFactorioCommand.spec.js @@ -0,0 +1,18 @@ +const isFactorioCommand = require("./isFactorioCommand"); +const assert = require("assert"); + +describe("inventoryImports/isFactorioCommand.js()", function(){ + it("Returns false if input is not a factorio command", function(){ + assert(isFactorioCommand({}) == false, "This is not even a string so should return false"); + assert(isFactorioCommand("Howdy boys!") == false, "This is not a command so should return false"); + assert(isFactorioCommand("/c this ain't a command because its invalid LUA") == false, "This is not valid LUA so it should return false"); + assert(isFactorioCommand("Trying to trick the system /c game.print('Hello world!')") == false, "this is still not a command"); + + }); + it("Returns true if it is a valid command", function(){ + assert(isFactorioCommand("/c game.print('hello world')"), "This is a valid command, should output true not false") + }); + it("Fails to understand that leading spaces are valid", function(){ + assert(isFactorioCommand(" /c game.print('Hello world!')") == false, "This should be true but often returns false unintentionally. If you see this you probably fixed the bug so change this test, OK?") + }); +}); \ No newline at end of file diff --git a/sharedPlugins/inventoryImports/isJson.js b/sharedPlugins/inventoryImports/isJson.js new file mode 100644 index 00000000..597564df --- /dev/null +++ b/sharedPlugins/inventoryImports/isJson.js @@ -0,0 +1,11 @@ +module.exports = function(x){ + if(x === false || x === true || x === null) return true; // === false is valid JSON, so is true and null + var isNotJson = false + try{ + z = JSON.parse(x); + } catch (e) { + isNotJson = true; + } + + return !isNotJson; +} \ No newline at end of file diff --git a/sharedPlugins/inventoryImports/isJson.spec.js b/sharedPlugins/inventoryImports/isJson.spec.js new file mode 100644 index 00000000..db0f2657 --- /dev/null +++ b/sharedPlugins/inventoryImports/isJson.spec.js @@ -0,0 +1,20 @@ +const isJson = require("./isJson"); +const assert = require("assert"); + +describe("inventoryImports/isJson.js()", function(){ + it("checks if a thing is valid JSON", function(){ + assert(isJson("string") === false); + assert(isJson("{js_object_notation='very false'}") === false); + assert(isJson('{ "name":"John", "age":31, "city":"New York" }') === true, "This is valid JSON, lib disagrees"); + }); + it("recognizes that true and false are both valid JSON", function(){ + assert(isJson(true) === true); + assert(isJson(false) === true); + }); + it("Handles null as valid JSON (after spec)", function(){ + assert(isJson(null) === true); + }); + it("Recognizes that JSON doesn't allow trailing commas (,)", function(){ + assert(isJson('["a", "b", ]') === false, "JSON should not allow trailing commas"); + }); +}); diff --git a/sharedPlugins/inventoryImports/lua commands.lua b/sharedPlugins/inventoryImports/lua commands.lua new file mode 100644 index 00000000..d3668a33 --- /dev/null +++ b/sharedPlugins/inventoryImports/lua commands.lua @@ -0,0 +1,180 @@ +-- Lua cheatsheet + +/c game.write_file("test.txt",serpent.block(game.players["Danielv123"].get_quickbar().get_contents())) +/c game.write_file("test.txt",serpent.block(game.players["Danielv123"].get_inventory(defines.inventory.player_main).get_contents())) + +-- print JSON of entire inventory and request slots to file +-- TODO: Get requester slot data as well + +-- requester slot data is game.players.character.get_request_slot(slot) +-- returns {name="copper-plate", count=47} or nil if nothing is set +-- maybe game.players.character.item_requests? http://lua-api.factorio.com/latest/LuaEntity.html#LuaEntity.item_requests + +/c +local requests = {} +for i = 1, game.player.force.character_logistic_slot_count do + requests[i] = game.players["Danielv123"].character.get_request_slot(i) +end + +local o = "Requests: " +for k,v in pairs(requests) do + o = o..v["name"]..": "..v["count"]..", " +end +game.print(o) + +-- sum up our inventory and request slots and print it as valid JSON +-- turns out the valid part is important +/c +local o = '{"players":{' +local notFirst = false +for a,b in pairs(game.players) do + if b.connected then + if notFirst then + o = o .. ',' + else + notFirst = true + end + local inventory = game.players[a].get_inventory(defines.inventory.player_main).get_contents() + local quickbar = game.players[a].get_quickbar().get_contents() + --[[ Get contents of requester slots into a table ]] + local requests = {} + for i = 1, game.players[a].force.character_logistic_slot_count do + requests[i] = game.players[a].character.get_request_slot(i) + end + + o = o..'"'..a..'":{"inventory":{' + --[[ generate JSON table ]] + local addComma = false + for k,v in pairs(inventory) do + if addComma then + o = o..',' + else + addComma = true + end + o = o..'"'..k..'":'..v + end + for k,v in pairs(quickbar) do + if addComma then + o = o..',' + else + addComma = true + end + o = o..'"'..k..'":'..v + end + + o = o..'},"requestSlots":{' + addComma = false + for k,v in pairs(requests) do + if addComma then + o = o..',' + else + addComma = true + end + o = o..'"'..v['name']..'":'..v['count'] + end + o = o..'}}' --[[ We need a comma after these two if there is more than 1 player, handled up top by notFirst]] + end +end +o = o.."}}" +game.write_file("t.txt", o) +game.print(o) + +--[[ +Example output +{"players":{"1":{"inventory":{"stone":12,"iron-ore":120,"raw-wood":10,"iron-plate":8,"steel-plate":480},"requestSlots":{}}}} + +Minification procedure: + +Run through https://mothereff.in/lua-minifier +Replace ' with '+"'"+' +Prepend /silent-command +remove game.print(o) +replace "t.txt" with "'+outputFile+'" +enclose in '' + +]] + +-- minified strings (newest on the top) +local a='+"'"+'{"players":{'+"'"+'local b=false;for c,d in pairs(game.players)do if d.connected then if b then a=a..'+"'"+','+"'"+'else b=true end;local e=game.players[c].get_inventory(defines.inventory.player_main).get_contents()local f=game.players[c].get_quickbar().get_contents()local g={}for h=1,game.players[c].force.character_logistic_slot_count do g[h]=game.players[c].character.get_request_slot(h)end;a=a..'+"'"+'"'+"'"+'..c..'+"'"+'":{"inventory":{'+"'"+'local i=false;for j,k in pairs(e)do if i then a=a..'+"'"+','+"'"+'else i=true end;a=a..'+"'"+'"'+"'"+'..j..'+"'"+'":'+"'"+'..k end;for j,k in pairs(f)do if i then a=a..'+"'"+','+"'"+'else i=true end;a=a..'+"'"+'"'+"'"+'..j..'+"'"+'":'+"'"+'..k end;a=a..'+"'"+'},"requestSlots":{'+"'"+'i=false;for j,k in pairs(g)do if i then a=a..'+"'"+','+"'"+'else i=true end;a=a..'+"'"+'"'+"'"+'..k['+"'"+'name'+"'"+']..'+"'"+'":'+"'"+'..k['+"'"+'count'+"'"+']end;a=a..'+"'"+'}}'+"'"+'end end;a=a.."}}"game.write_file("'+outputFile+'",a) +-- offline playes +local a="{"for b,c in pairs(game.players)do if c.connected then local d=game.players[b].get_inventory(defines.inventory.player_main).get_contents()local e=game.players[b].get_quickbar().get_contents()local f={}for g=1,game.players[b].force.character_logistic_slot_count do f[g]=game.players[b].character.get_request_slot(g)end;a=a..b..":{inventory:{"for h,i in pairs(d)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;for h,i in pairs(e)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;a=a.."},requestSlots:{"for h,i in pairs(f)do a=a.."['+"'"+'"..i["name"].."'+"'"+']:"..i["count"]..", "end;a=a.."}},"end end;a=a.."}"game.print(a) +-- multiple players +local a="{"for b,c in pairs(game.players)do local d=game.players[b].get_inventory(defines.inventory.player_main).get_contents()local e=game.players[b].get_quickbar().get_contents()local f={}for g=1,game.players[b].force.character_logistic_slot_count do f[g]=game.players[b].character.get_request_slot(g)end;a=a..b..":{inventory:{"for h,i in pairs(d)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;for h,i in pairs(e)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;a=a.."},requestSlots:{"for h,i in pairs(f)do a=a.."['+"'"+'"..i["name"].."'+"'"+']:"..i["count"]..", "end;a=a.."}},"end;a=a.."}"game.print(a) +local a=game.players["Danielv123"].get_inventory(defines.inventory.player_main).get_contents()local b=game.players["Danielv123"].get_quickbar().get_contents()local c={}for d=1,game.players["Danielv123"].force.character_logistic_slot_count do c[d]=game.players["Danielv123"].character.get_request_slot(d)end;local e="{inventory:{"for f,g in pairs(a)do e=e.."['+"'"+'"..f.."'+"'"+']:"..g..","end;for f,g in pairs(b)do e=e.."['+"'"+'"..f.."'+"'"+']:"..g..","end;e=e.."},requestSlots:{"for f,g in pairs(c)do e=e.."['+"'"+'"..g["name"].."'+"'"+']:"..g["count"]..", "end;e=e.."}}"game.print(e) +local inventory = game.players["Danielv123"].get_inventory(defines.inventory.player_main).get_contents()local quickbar = game.players["Danielv123"].get_quickbar().get_contents()local requests = {}for i = 1, game.player.force.character_logistic_slot_count do requests[i] = game.players["Danielv123"].character.get_request_slot(i) end local o = "{inventory:{" for k,v in pairs(inventory) do o = o.."['+"'"+'"..k.."'+"'"+']:"..v.."," end for k,v in pairs(quickbar) do o = o.."['+"'"+'"..k.."'+"'"+']:"..v.."," end o = o.."},requestSlots:{" for k,v in pairs(requests) do o = o.."['+"'"+'"..v["name"].."'+"'"+']:"..v["count"]..", " end o = o.."}}" game.write_file("test.txt", o) + + +-- insert items in players inventory and export leftovers +-- name should be players[i] + +/c +local name = "1" +local items = {["iron-ore"]=30, ["steel-plate"]=120} +local items_failed = {} +if(game.players[name] and game.players[name].connected) then + for item,count in pairs(items) do + local i = game.players[name].insert{name=item,count=count} + if not items_failed[name] then + items_failed[name] = {} + end + if items_failed[name][item] then + items_failed[name][item] = items_failed[name][item] + count - i + else + items_failed[name][item] = count - i + end + end + game.write_file("t.txt", serpent.line({["exports"]=items_failed}, {["comment"]=false, ["compact"]=true})) +end + +--[[ +Writes something like + +{exports={["1"]={["iron-ore"]=0,["steel-plate"]=0}}} + +converting to JS would be +replace "[" and "]" with ""??? +replace "=" with ":" +]] + +--[[ +JSON edition + +{"exports":{"1":[{"name":"iron-ore","count":0},{"name":"steel-plate","count":0}]}} + +Should always return valid JSON +]] +/c +local name = "1" +local items = {["iron-ore"]=30, ["steel-plate"]=120} +if(game.players[name] and game.players[name].connected) then + local o = '{"exports":{"'..name..'":[' + local addComma = false + for item,count in pairs(items) do + local i = game.players[name].insert{name=item,count=count} + if addComma then + o = o..',' + else + addComma = true + end + o = o .. '{"name":"'..item..'","count":'..count - i..'}' + end + o = o .. ']}}' + game.write_file("t.txt", o) +end + +--[[ +Minification procedure: + +Run through https://mothereff.in/lua-minifier +Replace ' with '+"'"+' +Prepend /silent-command +replace "t.txt" with "'+outputFile+'" +enclose in '' +]] + + +-- JSON outputFile +local a="1"local b={["iron-ore"]=30,["steel-plate"]=120}local c={}if game.players[a]and game.players[a].connected then local d='+"'"+'{"exports":{"'+"'"+'..a..'+"'"+'":['+"'"+'local e=false;for f,g in pairs(b)do local h=game.players[a].insert{name=f,count=g}if e then d=d..'+"'"+','+"'"+'else e=true end;d=d..'+"'"+'{"name":"'+"'"+'..f..'+"'"+'","count":'+"'"+'..g-h..'+"'"+'}'+"'"+'end;d=d..'+"'"+']}}'+"'"+'game.write_file("'+outputFile+'",d)end +-- output to serpented LUA table +local a="Danielv123"local b={["iron-ore"]=30,["steel-plate"]=120}local c={}if game.players[a]and game.players[a].connected then for d,e in pairs(b)do local f=game.players[a].insert{name=d,count=e}if not c[a]then c[a]={}end;if c[a][d]then c[a][d]=c[a][d]+e-f else c[a][d]=e-f end end;game.write_file("'+outputFile+'",serpent.line(c,{["comment"]=false,["compact"]=true}))end + diff --git a/static/slaveDisplay.js b/static/slaveDisplay.js index 959491b5..c901e1d2 100644 --- a/static/slaveDisplay.js +++ b/static/slaveDisplay.js @@ -42,6 +42,10 @@ setTimeout(function() { xmlhttp.send(); }, 0) +let chartIgnoreList = [ + "water", + "steam" +] // ID of slave, ID of canvasjs div without # function makeGraph(slaveID, selector) { @@ -56,7 +60,7 @@ function makeGraph(slaveID, selector) { } let chartData = []; for(let o = 0; o < itemNames.length; o++) { - if(itemNames[o] != "water"){ + if(!chartIgnoreList.includes(itemNames[o])){ chartData[chartData.length] = generateLineChartArray(data, itemNames[o]); } }