var fs = require('fs-extra'); var https = require('follow-redirects').https; var needle = require("needle"); var child_process = require('child_process'); var path = require('path'); var syncRequest = require('sync-request'); var request = require("request") var ncp = require('ncp').ncp; var Rcon = require('simple-rcon'); var hashFiles = require('hash-files'); var libomega = require("./libomega.js"); // require config.json var config = require('./config'); var global = {}; if (!fs.existsSync("./instances/")) { fs.mkdirSync("instances"); } if (!fs.existsSync("./sharedPlugins/")) { fs.mkdirSync("sharedPlugins"); } if (!fs.existsSync("./sharedMods/")) { fs.mkdirSync("sharedMods"); } const instance = process.argv[3]; const instancedirectory = './instances/' + instance; const command = process.argv[2]; var instanceInfo = {}; // Functions function deleteFolderRecursive(path) { if (fs.existsSync(path)) { fs.readdirSync(path).forEach(function (file, index) { var curPath = path + "/" + file; if (fs.lstatSync(curPath).isDirectory()) { // recurse deleteFolderRecursive(curPath); } else { // delete file fs.unlinkSync(curPath); } }); fs.rmdirSync(path); } }; // simple string hasher String.prototype.hashCode = function () { var hash = 0; if (this.length == 0) return hash; for (let i = 0; i < this.length; i++) { char = this.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32bit integer } return hash; } // function to handle sending commands into the game function messageInterface(command, callback) { // try to save us if you send a buffer instead of string if(typeof command == "object") { command = command.toString('utf8'); } if(typeof command == "string" && client && client.exec && typeof client.exec == "function") { try { client.exec(command, callback); } catch (err) { console.log(err); if(typeof callback == "function"){ callback(err); } } } } // handle commandline parameters if (!command || command == "help" || command == "--help") { console.error("Usage: ") console.error("node client.js start [instance name]") console.error("node client.js list") console.error("node client.js delete [instance]") console.error("To download the latest version of the Clusterio lua mod, do") console.error("node client.js download") process.exit(1) } else if (command == "list") { console.dir(getDirectories("./instances/")); process.exit(1) } else if (command == "delete") { if (!process.argv[3]) { console.error("Usage: node client.js delete [instance]"); process.exit(1) } else if (typeof process.argv[3] == "string" && fs.existsSync("./instances/" + process.argv[3]) && process.argv[3] != "/" && process.argv[3] != "") { deleteFolderRecursive("./instances/" + process.argv[3]); console.log("Deleted instance " + process.argv[3]) process.exit(1) } else { console.error("Instance not found: " + process.argv[3]); process.exit(1) } } else if (command == "download") { console.log("Downloading mods..."); // get JSON data about releases let res = syncRequest('GET', 'https://api.github.com/repos/Danielv123/factorioClusterioMod/releases', {"headers":{"User-Agent":"Fuck you for requiring user agents!"}}); let url = JSON.parse(res.getBody())[0].assets[0].browser_download_url; let name = JSON.parse(res.getBody())[0].assets[0].name; if(url) { console.log(url); var file = fs.createWriteStream("sharedMods/"+name); var request = https.get(url, function(response) { response.pipe(file); console.log("Downloaded "+name) }); } } else if (command == "start" && typeof instance == "string" && instance != "/" && !fs.existsSync(instancedirectory)) { // if instance does not exist, create it console.log("Creating instance..."); fs.mkdirSync(instancedirectory); fs.mkdirSync(instancedirectory + "/script-output/"); fs.mkdirSync(instancedirectory + "/saves/"); fs.writeFileSync(instancedirectory + "/script-output/output.txt", ""); fs.writeFileSync(instancedirectory + "/script-output/orders.txt", ""); fs.writeFileSync(instancedirectory + "/script-output/txbuffer.txt", ""); fs.mkdirSync(instancedirectory + "/mods/"); // fs.symlinkSync('../../../sharedMods', instancedirectory + "/mods", 'junction') // This is broken because it can only take a file as first argument, not a folder fs.writeFileSync(instancedirectory + "/config.ini", "[path]\r\n\ read-data=__PATH__executable__/../../data\r\n\ write-data=__PATH__executable__/../../../instances/" + instance + "\r\n\ "); fs.copySync('sharedMods', instancedirectory + "/mods") let instconf = { "factorioPort": Math.floor(Math.random() * 65535), "clientPort": Math.floor(Math.random() * 65535), "clientPassword": Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 8), } console.log("Clusterio | Moving shared mods...") console.log(instconf) // create instance config fs.writeFileSync(instancedirectory + "/config.json", JSON.stringify(instconf, null, 4)); let name = "Clusterio instance: " + instance; if (config.username) { name = config.username + "'s clusterio " + instance; } let serversettings = { "name": name, "description": config.description, "tags": ["clusterio"], "max_players": "20", "visibility": config.visibility, "username": config.username, "password": config.password, "token": config.usertoken, "game_password": config.game_password, "verify_user_identity": config.verify_user_identity, "admins": [config.username], "allow_commands": config.allow_commands, "autosave_interval": 10, "autosave_slots": 5, "afk_autokick_interval": 0, "auto_pause": config.auto_pause, } fs.writeFileSync(instancedirectory + "/server-settings.json", JSON.stringify(serversettings, null, 4)); let createSave = child_process.spawnSync( './' + config.factorioDirectory + '/bin/x64/factorio', [ '-c', instancedirectory + '/config.ini', '--create', instancedirectory + '/saves/save.zip', ] ) console.log("Instance created!") } else if (command == "start" && typeof instance == "string" && instance != "/" && fs.existsSync(instancedirectory)) { // Exit if no instance specified (it should be, just a safeguard) if(instancedirectory != "./instances/undefined"){ var instanceconfig = require(instancedirectory + '/config'); instanceconfig.unique = instanceconfig.clientPassword.hashCode(); } else { process.exit(1) } console.log("Deleting .tmp.zip files"); let savefiles = fs.readdirSync(instancedirectory + "/saves/"); for(i = 0; i < savefiles.length; i++){ if(savefiles[i].substr(savefiles[i].length - 8, 8) == ".tmp.zip") { fs.unlinkSync(instancedirectory + "/saves/" + savefiles[i]); } } console.log("Deleting logs"); // clean old log file to avoid crash // file exists, delete so we don't get in trouble try { fs.unlinkSync(instancedirectory+'/factorio-current.log') } catch (err){ if(err){ console.log(err); } else { console.log("Clusterio | Deleting old logs...") } } // move mods from ./sharedMods to the instances mod directory console.log("Clusterio | Moving shared mods..."); fs.copySync('sharedMods', instancedirectory + "/mods"); process.on('SIGINT', function () { console.log("Caught interrupt signal"); messageInterface("/quit"); }); // Spawn factorio server //var serverprocess = child_process.exec(commandline) getNewestFile(instancedirectory + "/saves/", fs.readdirSync(instancedirectory + "/saves/"),function(latestSave) { var serverprocess = child_process.spawn( './' + config.factorioDirectory + '/bin/x64/factorio', [ '-c', instancedirectory + '/config.ini', '--start-server', latestSave.file, '--rcon-port', instanceconfig.clientPort, '--rcon-password', instanceconfig.clientPassword, '--server-settings', instancedirectory + '/server-settings.json', '--port', instanceconfig.factorioPort ], { 'stdio': ['pipe', 'pipe', 'pipe'] } ) serverprocess.on('close', (code) => { console.log(`child process exited with code ${code}`); process.exit(); }); /* serverprocess.stdout.on('data', (chunk) => { console.log('OUT: ' + chunk); })*/ serverprocess.stderr.on('data', (chunk) => { console.log('ERR: ' + chunk); }) // connect to the server with rcon // IP, port, password client = new Rcon({ host: 'localhost', port: instanceconfig.clientPort, password: instanceconfig.clientPassword, timeout: 0 }); // check the logfile to see if the RCON interface is running as there is no way to continue without it // we read the log every 2 seconds and stop looping when we start connecting to factorio function checkRcon() { fs.readFile(instancedirectory+"/factorio-current.log", function (err, data) { // if (err) console.log(err); if(data && data.indexOf('Starting RCON interface') > 0){ client.connect(); } else { setTimeout(function(){ checkRcon(); },2000); } }); } setTimeout(checkRcon, 5000); client.on('authenticated', function () { console.log('Clusterio | Authenticated!'); instanceManagement(); // start using rcons }).on('connected', function () { console.log('Clusterio | Connected!'); // getID(); }).on('disconnected', function () { console.log('Clusterio | Disconnected!'); // now reconnect client.connect(); }); // set some globals confirmedOrders = []; lastSignalCheck = Date.now(); }) } function instanceManagement() { // load plugins and execute onLoad event let pluginDirectories = getDirectories("./sharedPlugins/"); let plugins = []; for(i=0; i { // log("Stdout: " + data); messageInterface(data.toString('utf8')); }); plugins[i].stderr.on("data", (data) => { log("STDERR: " + data); }) plugins[i].on('close', (code) => { log(`child process exited with code ${code}`); }); } // world IDs ------------------------------------------------------------------ hashMods(instance, function(modHashes){ setInterval(getID, 10000); getID() function getID() { messageInterface("/silent-command game.write_file('tempfile.txt', 'connected_players ' .. #game.connected_players .. '\\n', true, 0)", function(err) {setTimeout(function(){ // get array of lines in file if(fs.existsSync(instancedirectory + "/script-output/tempfile.txt")) { var data = fs.readFileSync(instancedirectory + "/script-output/tempfile.txt", "utf8").split("\n"); // delete when we are done fs.unlink(instancedirectory + "/script-output/tempfile.txt"); } // if we actually got anything from the file, proceed to categorize it if (data && data[0]) { while (data[0]) { let q = data[0].split(" "); // delete array element data.splice(0,1) if(q[0] == "connected_players" && Number(q[1]) != NaN) { instanceInfo.playerCount = q[1]; } } } else { instanceInfo.playerCount = 0; } var payload = { time: Date.now(), rconPort: instanceconfig.clientPort, rconPassword: instanceconfig.clientPassword, serverPort: instanceconfig.factorioPort, unique: instanceconfig.unique, publicIP: config.publicIP, // IP of the server should be global for all instances, so we pull that straight from the config mods:modHashes, playerCount: instanceInfo.playerCount || 0, } require('getmac').getMac(function (err, mac) { if (err) throw err payload.mac = mac // console.log(payload) needle.post(config.masterIP + ":" + config.masterPort + '/api/getID', payload, function (err, response, body) { if (response && response.body) { // In the future we might be interested in whether or not we actually manage to send it, but honestly I don't care. console.log(response.body) } }); }) },1000) }) } }) // Mod uploading and management ----------------------------------------------- // get mod names and hashes // string: instance, function: callback setTimeout(function(){hashMods(instance, uploadMods)}, 5000); function uploadMods(modHashes) { // [{modName:string,hash:string}, ... ] for(i=0;i newest.mtime.getTime()) { newest = { file : file, mtime : stats.mtime }; } if (checked == files.length) { callback(newest); } }); })(dir + file); } }); }