mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-12 15:04:52 +09:00
Initial draft of remoteMap 3.0
This commit is contained in:
1 parent
df7100ecba
commit
3a87aa2959
7 files changed
+243
-119
No files matched your search
File renamed without changes.
@@ -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",
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
File renamed without changes.
@@ -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"],
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
Reference in new issue
Block a user