diff --git a/sharedPlugins/remoteMap/chunkStore.spec.js b/sharedPlugins/remoteMap/chunkStore.spe_c.js similarity index 100% rename from sharedPlugins/remoteMap/chunkStore.spec.js rename to sharedPlugins/remoteMap/chunkStore.spe_c.js diff --git a/sharedPlugins/remoteMap/config.js b/sharedPlugins/remoteMap/config.js index f210afc1..10a2d9c0 100644 --- a/sharedPlugins/remoteMap/config.js +++ b/sharedPlugins/remoteMap/config.js @@ -13,4 +13,5 @@ module.exports = { description: modInfo.description, // We'll send everything in this file to your stdin. Beware. scriptOutputFileSubscription: "remoteMap.txt", + masterPlugin: "masterPlugin.js", } \ No newline at end of file diff --git a/sharedPlugins/remoteMap/index.js b/sharedPlugins/remoteMap/index.js index 9c03cc08..efc97116 100644 --- a/sharedPlugins/remoteMap/index.js +++ b/sharedPlugins/remoteMap/index.js @@ -1,130 +1,67 @@ const needle = require("needle"); -const ioClient = require("socket.io-client"); -const chunkStore = require("./chunkStore.js"); - -const moduleConfig = require("./config"); // not to be confused with clusterio config. This config is private to this plugin. - -class remoteMap { - constructor(slaveConfig, messageInterface){ - this.config = slaveConfig; +const debug = true +module.exports = class remoteMap { + constructor(mergedConfig, messageInterface, extras){ this.messageInterface = messageInterface; + this.config = mergedConfig; + this.socket = extras.socket; + this.instances = {}; - // initialize chunk database - this.chunkMap = new chunkStore(this.config.unique, 64, "./database/chunkStore/"); - this.chunkMap.onEntityChange(entity => { - // this.messageInterface("Entity changed: ") - // this.messageInterface(entity); - // check valid coordinates (dunno what could go wrong, but can never be too sure) - if(entity && entity.x !== undefined && !isNaN(Number(entity.x)) && entity.y !== undefined && !isNaN(Number(entity.y))){ - this.socket.emit("sendEntity", entity); + this.socket.on("hello", () => this.socket.emit("registerMapServer")); + + this.socket.on("createEntity", req => { + // req = {name:"", position:{x,y}, ?direction:4} + // game.player.surface.create_entity{name="small-ship-wreck", position={game.player.position.x-2, game.player.position.y+1}, direction=..., force=...} + this.messageInterface("placing "+JSON.stringify(req)); + if(req && req.name && typeof req.name == "string" && req.position && !isNaN(Number(req.position.x)) && !isNaN(Number(req.position.y))){ + // delete any entity in that spot first, just in case to avoid overlaps + this.messageInterface('/silent-command local toDelete = game.surfaces[1].find_entities({{'+req.position.x+','+req.position.y+'},{'+(req.position.x+0.5)+','+(req.position.y+0.5)+'}}) for i, entity in pairs(toDelete) do entity.destroy() end'); + // build the entity + let command = '/silent-command game.surfaces[1].create_entity{name="'+req.name+'", position={'+Number(req.position.x)+', '+Number(req.position.y)+'}, force="player"' + if(req.direction) command += ', direction='+req.direction; + command += '}'; + this.messageInterface(command); // execute command } - }); + }) + this.socket.on("deleteEntity", req => { + // req: {position: {x, y}} + this.messageInterface('/silent-command local toDelete = game.surfaces[1].find_entities({{'+req.position.x+','+req.position.y+'},{'+(req.position.x+0.5)+','+(req.position.y+0.5)+'}}) for i, entity in pairs(toDelete) do entity.destroy() end'); + }) + this.socket.on("getChunk", async (data, callback) => { + callback(await this.messageInterface(`/silent-command remote.call("remoteMap", "exportChunk", ${data.x}, ${data.y})`)); + }) - // set up websocket communication and handle requests from web interface users (which are going through master) - // socket should be a global - this.socket = ioClient("http://"+this.config.masterIP+":"+this.config.masterPort); - this.socket.on("hello", data => { - this.socket.emit("registerSlaveMapper", {instanceID: this.config.unique}); - setInterval(()=>{ - this.socket.emit("heartbeat"); // send our heartbeat to prevent being assumed dead - },10000); - }); - this.socket.on("getChunk", req => { - this.messageInterface("getChunk is not supported!"); - /*this.chunkMap.getChunk(req.x, req.y).then(chunk => { - chunk.requesterID = req.requesterID; - this.socket.emit("sendChunk", chunk); - });*/ - }); - this.socket.on("getEntity", req => { - // check for valid coordinates - if(req.x !== undefined && !isNaN(Number(req.x)) && req.y !== undefined && !isNaN(Number(req.y))){ - this.chunkMap.getEntity(req.x, req.y).then(entities => { - if(entities && entities.length > 0){ - entities.forEach(entity => { - this.socket.emit("sendEntity", entity); - }); + // detect new entities created/deleted on the server and update the master + setInterval(async ()=>{ + let data = await this.messageInterface(`/silent-command remote.call("remoteMap", "exportTiles")`) + if(data.length > 1) this.socket.emit("updateEntities", data) + }, 2000) + } + 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 { - this.messageInterface("socket.getEntity triggered called with invalid parameters"); + resolve(instance); } }); - // both placing and deleting entities via the web interface - this.socket.on("placeEntity", req => { - // game.player.surface.create_entity{name="small-ship-wreck", position={game.player.position.x-2, game.player.position.y+1}, direction=..., force=...} - this.messageInterface("placing "+JSON.stringify(req)); - if(req.name && typeof req.name == "string" && req.position && !isNaN(Number(req.position.x)) && !isNaN(Number(req.position.y))){ - if(req.name == "deleted"){ // {{'+req.position.x+','+req.position.y+'},{'+req.position.x+','+req.position.y+'}} - // delete entity from game world using LUA command - this.messageInterface('/silent-command local toDelete = game.surfaces[1].find_entities({{'+req.position.x+','+req.position.y+'},{'+(req.position.x+1)+','+(req.position.y+1)+'}}) for i, entity in pairs(toDelete) do entity.die() end'); - // this position is now empty, delete whatever was there from DB - this.deleteEntity(req.position.x, req.position.y); - } else { - let command = '/silent-command game.surfaces[1].create_entity{name="'+req.name+'", position={'+Number(req.position.x)+', '+Number(req.position.y)+'}, force="neutral"' - if(req.direction) command += ', direction='+req.direction; - command += '}'; - this.messageInterface(command); // execute command - - // add entity to the chunkStore database because create_entity does not trigger any events - let name = req.name; - let entity = {name}; - if(typeof req.direction == "string"){ - entity.rot = req.direction; - } - this.addEntity(req.position.x, req.position.y, entity); - } - } - }); - - this.messageInterface("/silent-command game.print('"+moduleConfig.name+" version "+moduleConfig.version+" enabled')"); } - addEntity(x,y,entity){ // add entity to database - this.chunkMap.setEntity(Number(x), Number(y), entity).then(data => { - this.messageInterface("Added "+entity.name+" to data "+Number(x)+", "+Number(y)); - }).catch((err)=>this.messageInterface(err)); - } - deleteEntity(x,y){ // remove entity from database - // this position is now empty, delete whatever was there from DB - this.chunkMap.setEntity(x, y, "delete this entity").then(() => { - this.messageInterface("Deleted entity at "+x+", "+y); - }).catch((err)=>this.messageInterface(err)); - } - scriptOutput(data){ - if(data){ - this.messageInterface(data); - // express-transport-belt,-35.5,-14.5,[...] - data = data.split(","); - if(data && data[0] == undefined){ - this.messageInterface("empty: "); - this.messageInterface(data); - return - } - let name = data[0]; - let xPos = data[1]; - let yPos = data[2]; - if(xPos && yPos && name){ - if(name == "deleted"){ - // this position is now empty, delete whatever was there from DB - this.deleteEntity(xPos, yPos); - } else { - let entity = {name}; - for(let i = 3; i < data.length; i++){ - if(data[i].includes("=")){ - let kv = data[i].split("="); - entity[kv[0]] = kv[1]; - } - } - // save this entity in the database - this.addEntity(xPos, yPos, entity); - /* this.chunkMap.setEntity(xPos, yPos, entity).then(data => { - this.messageInterface("Added "+name+" to data "+xPos+", "+yPos); - }).catch((err)=>this.messageInterface(err)); */ - } - } - - } - } -} -module.exports = remoteMap; +} \ No newline at end of file diff --git a/sharedPlugins/remoteMap/index.spec.js b/sharedPlugins/remoteMap/index_old.spe_c.js similarity index 100% rename from sharedPlugins/remoteMap/index.spec.js rename to sharedPlugins/remoteMap/index_old.spe_c.js diff --git a/sharedPlugins/remoteMap/info.json b/sharedPlugins/remoteMap/info.json index e60aaa47..00192c81 100644 --- a/sharedPlugins/remoteMap/info.json +++ b/sharedPlugins/remoteMap/info.json @@ -2,7 +2,7 @@ { "name": "remoteMap", "title": "Clusterio remote map", -"version": "2.2.0", +"version": "3.0.0", "date": "18-11-2017", "author": "Danielv123", "dependencies": ["base >= 0.15"], diff --git a/sharedPlugins/remoteMap/masterPlugin.js b/sharedPlugins/remoteMap/masterPlugin.js new file mode 100644 index 00000000..e96cffa8 --- /dev/null +++ b/sharedPlugins/remoteMap/masterPlugin.js @@ -0,0 +1,171 @@ +const fs = require("fs-extra"); +const path = require("path"); +const sanitizer = require('sanitizer'); + +const editorSockets = []; + +class masterPlugin { + constructor({ config, pluginConfig, pluginPath, socketio, express }) { + this.config = config; + this.pluginConfig = pluginConfig; + this.pluginPath = pluginPath; + this.io = socketio; + this.app = express; + + this.clients = {}; + this.slaves = {}; + + // initialize token auth module + this.authenticate = require("./../../lib/authenticate.js")(config); + + // expose UI elements embedded in the master + //this.ui = require("./js/ui.js").ui; + + // handle websocket connections + this.io.on("connection", socket => { + let instanceID = "unknown"; + socket.on("registerSlave", data => { + if (data.instanceID && !isNaN(Number(data.instanceID))) { + instanceID = data.instanceID; + this.slaves[instanceID] = socket; + } + socket.on("updateEntities", data => { + console.log(data) + if(data){ + // transform from weirdly deliminated string to entity object + let entities = data + .split("\n") + .filter(str => str.length > 0) + .map(ent => ent.replace(/[-]/g, "_")) // replace - with _ + .map(str => str + .split(",") + .filter(x => x.length > 0) + .map(prop => ({ + k: prop.split("=")[0], + v: prop.split("=")[1]}) + ) + ) + .map(arr => { + let obj = {} + arr.forEach(property => obj[property.k] = property.v) + return obj + }) + console.log(entities) + editorSockets.forEach(sock => sock.emit("updateEntity", {entities})) + } + }) + }); + socket.on("registerMapEditor", () => { + console.log("Registered mapEditor socket") + editorSockets.push(socket); + + setTimeout(()=>socket.emit("createEntity", {entity:{ + name:"fast_inserter", + position: {x:30,y:30}, + direction: 2, + }}),10000) + + socket.on("createEntity", data => { + this.slaves[Object.keys(this.slaves)[0]].emit("createEntity", data.entity) + }) + socket.on("deleteEntity", data => { + this.slaves[Object.keys(this.slaves)[0]].emit("deleteEntity", data.entity) + }) + socket.on("getChunk", (data, callback) => { + this.slaves[Object.keys(this.slaves)[0]].emit("getChunk", data, resp => { + // parse response + // |name:transport-belt,direction:2,x:99,y:1231|name:assembly-machine,direction:6,x:939,y:88 + console.log(resp) + callback(resp + .split("|") // split into entities + .map(str => str.replace("\n","")) // remove newlines that happen at the end of the last entity (maybe more as well???) + .filter(str => str.length > 0) // remove blank entities + .filter(str => !str.includes("tree") && // don't send entities that can't be rendered anyways + !str.includes("dead") && + !str.includes("rock") && + !str.includes("crude-oil") && + !str.includes("remnants") && + !str.includes("cliff") && + !str.includes("player") && + !str.includes("ore")) + .map(ent => ent.replace(/[-]/g, "_")) // replace - with _ + .map(ent => ent.split(",") // split entity into properties + .map(prop => [prop.split(":")[0], prop.split(":")[1]]) // split properties into KV pairs + ) + .map(entity => { // turn entities into objects + let entityObj = {} + entity.forEach(property => entityObj[property[0]] = property[1]) + return entityObj + }) + .filter(entity => Object.keys(entity).length > 1) + ) + }) + }) + + socket.on("disconnect", function () { + let i = editorSockets.indexOf(socket); + console.log("editorSocket " + (i + 1) + " disconnected, " + (editorSockets.length - 1) + " left"); + editorSockets.splice(i, 1); + }); + }); + socket.on("gameChat", async data => { + let chatLine = data.data.replace(/(\r\n\t|\n|\r\t)/gm, "").replace("\r", ""); + //if (typeof chatLine == "string") this.handleChatLine(chatLine, instanceID); + }); + }); + + } + async onExit() { + return; + } + async onLoadFinish({ plugins }) { + this.masterPlugins = plugins; + } + parseData(data, sharedData = {}) { + let parsedData = []; + data = data.split("|"); + data.forEach(player => { + if (player) { + let playerData = {}; + player = player.split(`~`); + player.forEach(kv => { + kv = kv.split(":"); + if (kv[1] === undefined || kv[0] === undefined) { + console.log(new Error(`Something is wrong! Key:${kv[0]} Value:${kv[1]}`)); + } else { + playerData[kv[0]] = kv[1].trim(); + } + }); + for (let k in sharedData) { + playerData[k] = sharedData[k]; + } + parsedData.push(playerData); + } + }); + return parsedData; + } +} +module.exports = masterPlugin; + +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/sharedPlugins/remoteMap/readme.md b/sharedPlugins/remoteMap/readme.md new file mode 100644 index 00000000..56511fc7 --- /dev/null +++ b/sharedPlugins/remoteMap/readme.md @@ -0,0 +1,15 @@ +# Remote Map + +A clusterio plugin for remotely exploring and editing the factorio world + +### Setup + +Make a database for storing entities + + docker run --name some-mariadb -e MYSQL_ROOT_PASSWORD=clusterio-remote-map --restart=unless-stopped -d mariadb:latest + +### Dev setup + +Make a database container and expose the ports + + docker run -p 3306:3306 --name some-mariadb -e MYSQL_ROOT_PASSWORD=clusterio-remote-map -d mariadb:latest