Compare commits

..
141 changed files with 1680 additions and 3530 deletions

No files matched your search

+7 -5
View File
@@ -11,9 +11,8 @@ pids
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directories used by tools like nyc and codecov
# Coverage directory used by tools like istanbul
coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
@@ -23,9 +22,9 @@ coverage
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
dist/
node_modules/*
!node_modules/_app
Factorio*
/factorio
@@ -63,5 +62,8 @@ sharedPlugins/scenario_loader/scenarios
# File used for marking plugins as disabled
sharedPlugins/*/DISABLED
# IDE
.idea/
\.cache-require-paths\.json
\.vs/
bin/
+8
View File
@@ -0,0 +1,8 @@
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
version: v1.12.0
ignore: {}
# patches apply the minimum changes required to fix a vulnerability
patch:
'npm:lodash:20180130':
- html5-validator > caporal > cli-table2 > lodash:
patched: '2018-07-03T06:07:55.532Z'
-3
View File
@@ -1,3 +0,0 @@
{
"CurrentProjectSetting": null
}
-8
View File
@@ -1,8 +0,0 @@
{
"ExpandedNodes": [
"",
"\\sharedPlugins"
],
"SelectedNode": "\\.gitignore",
"PreviewInSolutionExplorer": false
}
File diff suppressed because it is too large. Load diff
BIN
View File
Binary file not shown.
-18
View File
@@ -1,18 +0,0 @@
Changelog
=========
Version 1.2.1
-------------
- Updated node-factorio-api to v0.3.8 to fix mod downloads randomly breaking
([#229][#229].)
- Fixed SIGINT being sent twice to Factorio server when interrupted by CTRL+C
on Linux ([#217][#217].)
- Fixed package.json incorrectly reporting the license as ISC.
[#217]: https://github.com/clusterio/factorioClusterio/issues/217
[#229]: https://github.com/clusterio/factorioClusterio/issues/229
### Breaking Changes
- Added authentication to the socket.io server running on the master.
-19
View File
@@ -1,19 +0,0 @@
const needle = require("needle")
const config = require("./../config.json")
const cli = require("cli")
let options = cli.parse({
name: ["n", "Name of the player to delete, case sensitive", "string"],
token: [false, "Factorio auth token", "string", config.masterAuthToken]
})
if(!options.name){
cli.getUsage()
process.exit(1)
}
needle.post(config.masterIP+":"+config.masterPort+"/api/playerManager/deletePlayer", {
name: options.name,
token: options.token
}, (err, resp) => {
console.log(resp.body)
});
+7 -5
View File
@@ -2,20 +2,22 @@ FROM node:9.11.2
RUN apt-get update && apt install git curl tar -y
RUN mkdir factorioClusterio
RUN git clone -b master https://github.com/Danielv123/factorioClusterio.git && cd factorioClusterio && npm install --only=production
RUN cd factorioClusterio && curl -o factorio.tar.gz -L https://www.factorio.com/get-download/latest/headless/linux64 && tar -xf factorio.tar.gz
#RUN git clone -b master https://github.com/Danielv123/factorioClusterio.git && cd factorioClusterio && npm install
COPY . /factorioClusterio
RUN cd /factorioClusterio && npm install
RUN cd /factorioClusterio && curl -o factorio.tar.gz -L https://www.factorio.com/get-download/latest/headless/linux64 && tar -xf factorio.tar.gz && rm factorio.tar.gz
WORKDIR factorioClusterio
WORKDIR /factorioClusterio
RUN mkdir instances sharedMods
RUN cp config.json.dist config.json
RUN node client.js download
LABEL maintainer "Sir3lit@gmail.com"
LABEL maintainer "danielv@live.no"
EXPOSE 8080 34167
VOLUME /factorioClusterio/instances
VOLUME /factorioClusterio/sharedMods
VOLUME /factorioClusterio/sharedPlugins
CMD RCONPORT="$RCONPORT" FACTORIOPORT="$FACTORIOPORT" MODE="$MODE" node $MODE\.js start $INSTANCE
CMD RCONPORT="$RCONPORT" FACTORIOPORT="34167" MODE="$MODE" node $MODE\.js start $INSTANCE
+14 -14
View File
@@ -4,14 +4,6 @@
<br/>
<br/>
# Warning
Do not use master branch to run your Clusterio - master now is a development version and may not work
Use latest stable release: https://github.com/clusterio/factorioClusterio/releases/latest
Or clone latest stable branch that has name format `x.y.z`
# factorioClusterio
Discord for development/support/play: https://discord.gg/5XuDkje
@@ -86,7 +78,7 @@ Clusterio exploits one of the games logging features, game.write_file and RCON t
7. client.js on server2: We were allowed to import x of item y, run command /c remote.call("clusterio", "importMany", "{'copper-plate':120}")
This process works the same for both items and liquids, independent on what mods are used. Yes, modded items are fully supported.
This process works the same for both items and liquids, independant on what mods are used. Yes, modded items are fully supported.
Clusterio can also do a few other neat things, such as giving you access to epoch time, syncing player inventories between servers, keeping track of playtime (playerManager plugin), teleporting trains between servers (trainTeleports) and exporting tons of factorio related statistics to Prometheus for graphing in grafana.
@@ -99,11 +91,11 @@ Master and all slaves:
sudo curl -sL https://deb.nodesource.com/setup_9.x | sudo -E bash -
sudo apt install -y nodejs python-dev git wget curl tar build-essential
sudo npm install pm2 -g
git clone -b 1.2.x https://github.com/Danielv123/factorioClusterio.git
git clone https://github.com/Danielv123/factorioClusterio.git
cd factorioClusterio
curl -o factorio.tar.gz -L https://www.factorio.com/get-download/latest/headless/linux64
tar -xf factorio.tar.gz
npm install --only=production
npm install
cp config.json.dist config.json
node ./lib/npmPostinstall.js
@@ -234,9 +226,17 @@ Copy your config.json.dist to config.json and configure it.
Some systems don't let non root processes listen to ports below 1000. Either run with `sudo` or change config.json to use higher port numbers.
According to [this link](https://askubuntu.com/questions/839520/open-port-443-for-a-node-web-app) if you manually installed node.js following the above instructions, you may need to run the following command to fix this issue:
### InitializationError: First argument must be an abstract-leveldown compliant store
sudo setcap 'cap_net_bind_service=+ep' $(readlink -f $(which node))
do:
npm install linvodb3@3.25.1
if that does not suffice do
npm install levelup@1.3.9
If it still doesn't work, come over to our support discord linked at the top of this readme.
### Portforwarding doesn't work on the master server when running under WSL
@@ -279,7 +279,7 @@ node client.js list
```
git pull
npm install --only=production
npm install
```
3. Download the latest version of the factorioClusterioMod from its github repo
Binary file not shown.
+93 -97
View File
@@ -1,5 +1,4 @@
const fs = require('fs-extra');
const Tail = require('tail').Tail;
const https = require('follow-redirects').https;
const needle = require("needle");
const child_process = require('child_process');
@@ -7,18 +6,19 @@ const path = require('path');
const syncRequest = require('sync-request');
const request = require("request");
const ncp = require('ncp').ncp;
const Rcon = require('rcon-client-fork').Rcon;
const Rcon = require('rcon-client').Rcon;
const hashFiles = require('hash-files');
const _ = require('underscore');
const deepmerge = require("deepmerge");
const getMac = require('getmac').getMac;
const rmdirSync = require('rmdir-sync');
const ioClient = require("socket.io-client");
// internal libraries
const objectOps = require("lib/objectOps.js");
const fileOps = require("lib/fileOps");
const stringUtils = require("lib/stringUtils.js");
const configManager = require("lib/manager/configManager.js");
const hashFile = require('lib/hash').hashFile;
const objectOps = require("./lib/objectOps.js");
const fileOps = require("_app/fileOps");
const stringUtils = require("./lib/stringUtils.js");
const configManager = require("./lib/manager/configManager.js");
// argument parsing
const args = require('minimist')(process.argv.slice(2));
@@ -55,7 +55,6 @@ if(!config.masterAuthToken || typeof config.masterAuthToken !== "string"){
You can retrieve your auth token from the master in secret-api-token.txt after running it once.");
}
const needleOptionsWithTokenAuthHeader = {
compressed:true,
headers: {
'x-access-token': config.masterAuthToken
},
@@ -188,7 +187,7 @@ if (!command || command == "help" || command == "--help") {
if(tool == "mods"){
(async function(){try{
// do require down here to reduce master load time
const modManager = require("lib/manager/modManager.js")(config);
const modManager = require("./lib/manager/modManager.js")(config);
// allow managing mods
if(action == "list"){
@@ -228,7 +227,7 @@ if (!command || command == "help" || command == "--help") {
}
} else if(tool == "plugins"){
(async function(){try{
const pluginManager = require("lib/manager/pluginManager.js")(config);
const pluginManager = require("./lib/manager/pluginManager.js")(config);
if(action == "list"){
await pluginManager.listPlugins();
} else if(action == "add" || action == "install" || action == "download"){
@@ -301,7 +300,7 @@ if (!command || command == "help" || command == "--help") {
fs.mkdirSync(instancedirectory + "/mods/");
fs.mkdirSync(instancedirectory + "/instanceMods/");
fs.mkdirSync(instancedirectory + "/scenarios/");
ncp("lib/scenarios", path.resolve(instancedirectory, "scenarios"), err => {
ncp("./lib/scenarios", path.resolve(instancedirectory, "scenarios"), err => {
if (err) console.error(err)
});
@@ -318,7 +317,6 @@ write-data=${ path.resolve(config.instanceDirectory, instance) }\r\n
"clientPort": args["rcon-port"] || process.env.RCONPORT || Math.floor(Math.random() * 65535),
"__comment_clientPassword": "This is the rcon password. Its also used for making an instanceID. Make sure its unique and not blank.",
"clientPassword": args["rcon-password"] || Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 8),
"info": {}
}
console.log("Clusterio | Created instance with settings:")
console.log(instconf);
@@ -359,22 +357,19 @@ write-data=${ path.resolve(config.instanceDirectory, instance) }\r\n
'--rcon-port', Number(process.env.RCONPORT) || instconf.clientPort,
'--rcon-password', instconf.clientPassword,
], {
'stdio': ['pipe', 'pipe', 'pipe'],
'detached': process.platform === "linux",
'stdio': ['pipe', 'pipe', 'pipe']
}
);
factorio.stdout.on("data", data => {
data = data.toString("utf8").replace(/(\r\n\t|\n|\r\t)/gm,"");
console.log(data);
if(data.includes("Starting RCON interface")){
let client = new Rcon({
packetResponseTimeout: 200000,
maxPending: 5
});
let client = new Rcon();
client.connect({
host: 'localhost',
port: Number(process.env.RCONPORT) || instconf.clientPort,
password: instconf.clientPassword
password: instconf.clientPassword,
timeout: 5000
});
client.onDidAuthenticate(() => {
console.log('Clusterio | RCON Authenticated!');
@@ -472,8 +467,7 @@ write-data=${ path.resolve(config.instanceDirectory, instance) }\r\n
'--server-settings', instancedirectory + '/server-settings.json',
'--port', args.port || Number(process.env.FACTORIOPORT) || instanceconfig.factorioPort
], {
'stdio': ['pipe', 'pipe', 'pipe'],
'detached': process.platform === "linux",
'stdio': ['pipe', 'pipe', 'pipe']
}
);
@@ -499,10 +493,8 @@ write-data=${ path.resolve(config.instanceDirectory, instance) }\r\n
// connect to the server with rcon
if(true || process.platform != "linux"){
// IP, port, password
client = new Rcon({
packetResponseTimeout: 200000, // 200s, should allow for commands up to 1250kB in length
maxPending: 5
});
client = new Rcon();
// 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() {
@@ -513,6 +505,7 @@ write-data=${ path.resolve(config.instanceDirectory, instance) }\r\n
host: 'localhost',
port: args["rcon-port"] || Number(process.env.RCONPORT) || instanceconfig.clientPort,
password: args["rcon-password"] || instanceconfig.clientPassword,
timeout: 5000
});
} else {
setTimeout(function(){
@@ -564,18 +557,12 @@ write-data=${ path.resolve(config.instanceDirectory, instance) }\r\n
}
// ensure instancemanagement only ever runs once
var _instanceInitialized;
_.once(instanceManagement);
async function instanceManagement(instanceconfig) {
if (_instanceInitialized) return;
_instanceInitialized = true;
console.log("Started instanceManagement();");
/* Open websocket connection to master */
var socket = ioClient("http://"+config.masterIP+":"+config.masterPort+"?token="+config.masterAuthToken);
socket.on("error", err => {
console.log("SOCKET | Error: ", err);
});
var socket = ioClient("http://"+config.masterIP+":"+config.masterPort);
socket.on("hello", data => {
console.log("SOCKET | registering slave!");
socket.emit("registerSlave", {
@@ -585,12 +572,15 @@ async function instanceManagement(instanceconfig) {
setInterval(B=> socket.emit("heartbeat"), 10000);
// load plugins and execute onLoad event
const pluginManager = require("lib/manager/pluginManager.js")(config);
const pluginManager = require("./lib/manager/pluginManager.js")(config);
let pluginsToLoad = await pluginManager.getPlugins();
let plugins = [];
for(let i = 0; i < pluginsToLoad.length; i++){
let pluginLoadStarted = Date.now();
let log = function(message) {
console.log("Clusterio | "+ pluginsToLoad[i].name + " | " + message);
}
let combinedConfig = deepmerge(instanceconfig,config,{clone:true});
let pluginConfig = pluginsToLoad[i];
@@ -602,7 +592,7 @@ async function instanceManagement(instanceconfig) {
let pluginClass = require(path.resolve(pluginConfig.pluginPath, "index.js"));
plugins[i] = new pluginClass(combinedConfig, async function(data, callback){
if(data && data.toString('utf8')[0] != "/") {
console.log("Clusterio | "+ pluginsToLoad[i].name + " | " + data.toString('utf8'));
log(data.toString('utf8'));
return true;
} else if (data && data.toString('utf8')[0] == "/"){
return messageInterface(data.toString('utf8'), callback);
@@ -628,50 +618,37 @@ async function instanceManagement(instanceconfig) {
global.subscribedFiles[pluginConfig.scriptOutputFileSubscription] = true;
console.log("Clusterio | Registered file subscription on "+instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription);
fs.watch(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription, fileChangeHandler);
// run once in case a plugin wrote out information before the plugin loaded fully
// delay, so the socket got enough time to connect
setTimeout(()=> {
fileChangeHandler(false, pluginConfig.scriptOutputFileSubscription);
}, 500);
// send file contents to plugin for processing
function fileChangeHandler(eventType, filename) {
if(filename != null){
setTimeout(
()=>{
// get array of lines in file
let stuff = fs.readFileSync(instancedirectory + "/script-output/" + filename, "utf8").split("\n");
if(!pluginConfig.fileReadDelay || pluginConfig.fileReadDelay == 0) {
// only wipe the file on restart for now, should most likely be rotated during runtime too
fs.writeFileSync(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription, "");
let tail = new Tail(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription);
tail.on("line", function (data) {
plugins[i].scriptOutput(data);
});
} else {
fs.watch(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription, fileChangeHandler);
// run once in case a plugin wrote out information before the plugin loaded fully
// delay, so the socket got enough time to connect
setTimeout(() => {
fileChangeHandler(false, pluginConfig.scriptOutputFileSubscription);
}, 500);
// send file contents to plugin for processing
function fileChangeHandler(eventType, filename) {
if (filename != null) {
setTimeout(
() => {
// get array of lines in file
let stuff = fs.readFileSync(instancedirectory + "/script-output/" + filename, "utf8").split("\n");
// if you found anything, reset the file
if (stuff[0]) {
fs.writeFileSync(instancedirectory + "/script-output/" + filename, "");
}
for (let o = 0; o < stuff.length; o++) {
if (stuff[o] && !stuff[o].includes('\u0000\u0000')) {
try {
plugins[i].scriptOutput(stuff[o]);
} catch (e) {
console.error(e)
}
}
}
},
pluginConfig.fileReadDelay || 0
);
}
}
}
}
// if you found anything, reset the file
if (stuff[0]) {
fs.writeFileSync(instancedirectory + "/script-output/" + filename, "");
}
for(let o = 0; o < stuff.length; o++) {
if(stuff[o] && !stuff[o].includes('\u0000\u0000')) {
try{
plugins[i].scriptOutput(stuff[o]);
}catch(e){console.error(e)}
}
}
},
pluginConfig.fileReadDelay || 0
);
}
}
}
console.log(`Clusterio | Loaded plugin ${pluginsToLoad[i].name} in ${Date.now() - pluginLoadStarted}ms`);
} else {
// this plugin doesn't have a client portion. Maybe it runs on the master only?
@@ -693,8 +670,7 @@ async function instanceManagement(instanceconfig) {
publicIP: config.publicIP, // IP of the server should be global for all instances, so we pull that straight from the config
mods:modHashes,
instanceName: instance,
info: instanceconfig.info,
};
}
if(playerCount){
payload.playerCount = playerCount.replace(/(\r\n\t|\n|\r\t)/gm, "");
} else {
@@ -741,7 +717,7 @@ async function instanceManagement(instanceconfig) {
let payload = {
modName: modHashes[i].modName,
hash: modHashes[i].hash,
};
}
needle.post(config.masterIP + ":" + config.masterPort + '/api/checkMod', payload, needleOptionsWithTokenAuthHeader, function (err, response, body) {
if(err) console.error("Unable to contact master server /api/checkMod! Please check your config.json.");
if(response && body && body == "found") {
@@ -780,22 +756,42 @@ function hashMods(instanceName, callback) {
if(!callback) {
throw new Error("ERROR in function hashMods NO CALLBACK");
}
let modsDir = config.instanceDirectory+"/"+instanceName+"/mods/";
function hashMod(name) {
if (path.extname(name) != ".zip") {
// Can't hash unzipped mods, return null that's filtered out later
return null;
} else {
return hashFile(modsDir+name).then(hash => (
{modName: name, hash: hash}
));
function callback2(hash, modName){
hashedMods[hashedMods.length] = {
modName: modName,
hash: hash,
}
// check if this callback has ran once for each mod
if(hashedMods.length == instanceMods.length) {
callback(hashedMods);
}
//console.log(modname);
}
let hashedMods = [];
/*let mods = fs.readdirSync("./sharedMods/")*/
let instanceMods = fs.readdirSync(config.instanceDirectory+"/"+instanceName+"/mods/");
if(instanceMods.length == 0){
callback({}); // there are no mods installed, return an empty object
}
for(let o=0;o<instanceMods.length;o++) {
if(path.extname(instanceMods[o]) != ".zip") {
instanceMods.splice(instanceMods.indexOf(instanceMods[o]), 1); // remove element from array, we can't hash unzipped mods
}
}
let promises = fs.readdirSync(modsDir).map(hashMod);
Promise.all(promises).then(hashes => {
// Remove null entries from hashMod
callback(hashes.filter(entry => entry !== null));
});
for(let i=0; i<instanceMods.length; i++){
let path = config.instanceDirectory+"/"+instanceName+"/mods/"+instanceMods[i];
let name = instanceMods[i];
let options = {
files:path,
}
// options {files:[array of paths]}
hashFiles(options, function(error, hash) {
// hash will be a string if no error occurred
if(!error){
callback2(hash, name);
} else {
throw error;
}
});
}
}
+2 -10
View File
@@ -1,13 +1,5 @@
const needle = require("needle");
const cli = require("cli");
const config = require("./../config.json");
let options = cli.parse()
cli.setUsage(`node cluster_rcon.js /c game.player.force = "player"`)
if(process.argv.length < 3){
cli.getUsage()
process.exit(1)
}
const config = require("./config.json");
const masterIP = `${config.masterIP}:${config.masterPort}`;
const token = config.masterAuthToken
@@ -38,4 +30,4 @@ let killBiters = `/silent-command local surface=game.surfaces[1] for key, entity
// console.log(resp.body)
// });
// }, 1000*60*5);
})();
})();
-2
View File
@@ -47,9 +47,7 @@
"entriesPerSecond": 0.01666666667
},
"logItemTransfers": false,
"__comment6.5": "When the master is undersupplied, we have 3 different modes to 'fairly' distribute items. First to ask gets all, dole division using weird formula or experimental neural net tries to make stuff as fair as possible. Feel free to play around.",
"disableFairItemDistribution": false,
"useNeuralNetDoleDivider": true,
"uploadModsToMaster": true,
"msBetweenCommands": 10,
"allowRemoteCommandExecution": true,
+10
View File
@@ -0,0 +1,10 @@
toc:
- name: Master Server API
- clusterioMaster
- name: Type declarations
- itemStack
- name: Internal modules
description: |
Here are some helper functions for navigation.
- modManager
- getFactorioLocale
-57
View File
@@ -1,57 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const NodeInstance_1 = require("./NodeInstance");
const Point_1 = require("./Point");
const Grid_1 = require("./Grid");
/**
* Manages all nodes within the cluster
*/
class ClusterManager {
constructor() {
this.Grid = new Grid_1.Grid();
}
/**
* Generates a x*y grid of 1x1 nodes
* @param width How wide to make the node grid
* @param height How high to make the node grid
* @param nodeNamePrefix The prefix used for the initial node names. The name will be in the format <prefix>_x_y.
*/
GenerateBasicGridOfNodes(width, height, nodeNamePrefix) {
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
let newNode = new NodeInstance_1.NodeInstance(nodeNamePrefix + "-" + (x + 1) + "-" + (y + 1), new Point_1.Point(x, y), 1, 1);
this.Grid.AddNodeToGrid(newNode);
}
}
}
/**
* Assigns all nodes that currently do not have a server assigned to the given server
* @param server Server to assign all unassigned nodes to
*/
AssignAllUnassignedNodesToServer(server) {
this.CheckServerArray(server);
this.Grid.GetNodes()
.filter(node => { node.Server === null; })
.forEach(node => {
server.AddNodeInstance(node);
node.Server = server;
});
}
/**
* Assigns a node to a server
* @param x x co-ordinate of the node
* @param y y co-ordinate of the node
* @param serverName server name
*/
AssignNodeToServer(x, y, server) {
this._nodeArray[x][y].Server = server;
}
CheckServerArray(server) {
let s = this._servers[server.Name];
if (s == null) {
this._servers[server.Name] = server;
}
}
}
exports.ClusterManager = ClusterManager;
//# sourceMappingURL=ClusterManager.js.map
-112
View File
@@ -1,112 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const child_process = require("child_process");
const needle = require("needle");
const Helpers_1 = require("./Helpers");
const fs = require("fs");
const util = require("util");
/**
* Contains all interactions between the cluster model, the
* Clusterio master.js/client.js infrastructure and the operating system.
*/
class ClusterioMasterProxy {
constructor(masterIP, masterPort, masterAuthToken, serviceHostType) {
this._masterIP = `${masterIP}:${masterPort}`;
this._needleOptionsWithTokenAuthHeader = {
headers: {
'x-access-token': masterAuthToken,
},
};
}
async RunRconCommand(instanceId, command, logDescription) {
console.log("Running command: " + logDescription);
let request = { instanceID: instanceId, command };
let response = await needle("post", this._masterIP + "/api/runCommand", request, this._needleOptionsWithTokenAuthHeader);
}
async GetSlaves() {
let response = await needle("get", this._masterIP + "/api/slaves", this._needleOptionsWithTokenAuthHeader);
let body = response.body;
let slaves = new Array();
Object.entries(body).forEach((keyValuePair, index, array) => {
let slave = keyValuePair[1];
slave.id = keyValuePair[0];
slaves.push(slave);
});
return slaves;
}
async CreateNodeInstancesOnLocalServer(nodes) {
await Helpers_1.asyncForEach(nodes, async (node) => {
await this.CreateNodeInstanceOnLocalServer(node);
});
await Helpers_1.asyncForEach(nodes, async (node) => {
await this.CreateTeleportZonesForANodeInstance(node);
});
await this.sleep(1000);
await Helpers_1.asyncForEach(nodes, async (node) => {
await this.CreateTeleportRestrictionsForANodeInstance(node);
await this.sleep(100);
});
}
async CreateNodeInstanceOnLocalServer(node) {
console.log(`Creating instance ${node.Name}...`);
await this.CreateNodeInstanceOnLocalServerUsingPm2(node);
console.log("Waiting for node to connect to cluster master...");
while (true) {
await this.sleep(1000);
//TODO: Use /api/getSlaveMeta/
let slaves = await this.GetSlaves();
let slave = slaves.find((slave, index, array) => slave.instanceName === node.Name);
if (slave != null) {
node.ClusterioWorldId = slave.id;
break;
}
}
}
async CreateNodeInstanceOnLocalServerUsingPm2(node) {
let proc = child_process.spawn("pm2", ['start', '--name', node.Name, 'client.js', '--', 'start', node.Name]);
proc.stdout.on('data', (data) => { console.log(`stdout: ${data}`); });
proc.stderr.on('data', data => { console.log(`stderr: ${data}`); });
proc.on('error', error => { console.log(`error: ${error}`); });
}
async CreateNodeInstanceOnLocalServerUsingScreen(node) {
// first call of client.js start for a new node creates map from HotPatch scenario then exits.
let proc = child_process.spawn("screen", ['-dmS', node.Name, '-L', '-Logfile', node.Name + '.log', 'node', 'client.js', 'start', node.Name]);
proc.stdout.on('data', (data) => { console.log(`stdout: ${data}`); });
proc.stderr.on('data', data => { console.log(`stderr: ${data}`); });
proc.on('error', error => { console.log(`error: ${error}`); });
console.log("Waiting for creation of new map...");
// create awaiter for fs.readFile
const readFile = util.promisify(fs.readFile);
while (true) {
await this.sleep(1000);
let fileData = await readFile(node.Name + '.log');
if (fileData.includes('Instance created')) {
break;
}
}
// start node
proc = child_process.spawn("screen", ['-dmS', node.Name, '-L', '-Logfile', node.Name + '.log', 'node', 'client.js', 'start', node.Name]);
proc.stdout.on('data', data => { console.log(`stdout: ${data}`); });
proc.stderr.on('data', data => { console.log(`stderr: ${data}`); });
proc.on('error', error => { console.log(`error: ${error}`); });
}
async CreateTeleportZonesForANodeInstance(nodeInstance) {
await Helpers_1.asyncForEach(nodeInstance.TeleportZones.values(), async (zone) => {
let o = zone.GenerateTeleportZoneInFactorioCoordinates();
let cmd = `CreateZone('${o[0]}',${o[1]},${o[2]},${o[3]},${o[4]},${o[5]},true)`;
await this.RunRconCommand(nodeInstance.ClusterioWorldId, `/c remote.call('trainTeleports','runCode',\"${cmd}\")`, cmd);
});
}
async CreateTeleportRestrictionsForANodeInstance(nodeInstance) {
await Helpers_1.asyncForEach(nodeInstance.TeleportZones.values(), async (zone) => {
let o = zone.GenerateZoneRestriction();
let cmd = `CreateZoneRestriction(${o[0]},'${o[1]}','${o[2]}')`;
await this.RunRconCommand(nodeInstance.ClusterioWorldId, `/c remote.call('trainTeleports','runCode',\"${cmd}\")`, cmd);
});
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
exports.ClusterioMasterProxy = ClusterioMasterProxy;
//# sourceMappingURL=ClusterioMasterProxy.js.map
-8
View File
@@ -1,8 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/** The multiplier used to convert from the grid's coordinate system to factorio's.
* 1000 means a 1x1 grid node will have a factorio map size of 1000x1000. */
exports.GridToFactorioCoordinateSystemMultiplier = 1000;
/** The width of teleport zones when they get created. */
exports.TeleportZoneWidth = 50;
//# sourceMappingURL=Config.js.map
-177
View File
@@ -1,177 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const typescript_collections_1 = require("typescript-collections");
const Point_1 = require("./Point");
const TeleportZone_1 = require("./TeleportZone");
const TeleportZoneDirection_1 = require("./TeleportZoneDirection");
class Grid {
constructor() {
this._nodes = new typescript_collections_1.Dictionary();
this._minX = 0;
this._minY = 0;
this._maxX = 0;
this._maxY = 0;
}
GetNodes() {
return this._nodes.values();
}
/**
* Adds a node to the grid
* @param topLeftCoordinate The top left x,y coordinate of the node
* @param nodeInstance
*/
AddNodeToGrid(nodeInstance) {
let topLeftCoordinate = nodeInstance.TopLeftCoordinate;
let pointsToSave = new Array();
// check all points that make up nodeInstance for existing nodes
for (let x = topLeftCoordinate.X; x < topLeftCoordinate.X + nodeInstance.Width; x++) {
for (let y = topLeftCoordinate.Y; y < topLeftCoordinate.Y + nodeInstance.Height; y++) {
let currentPoint = new Point_1.Point(x, y);
let existingNode = this._nodes.getValue(currentPoint);
if (existingNode != null) {
throw new Error("Cannot add node to grid - collision detected with node '" + existingNode.Name + "'");
}
pointsToSave.push(currentPoint);
}
}
// no existing nodes found to assign node to these points
for (let i = 0; i < pointsToSave.length; i++) {
this._nodes.setValue(pointsToSave[i], nodeInstance);
}
//TODO: Refactor
//#region Set up teleport zones
// check surrounding points for nodes
// check north points
let y = topLeftCoordinate.Y - 1;
for (let x = topLeftCoordinate.X; x < topLeftCoordinate.X + nodeInstance.Width; x++) {
let currentPoint = new Point_1.Point(x, y);
let existingNode = this._nodes.getValue(currentPoint);
// found a node - set up teleport zone
if (existingNode != null) {
// Create/Update teleport node in existing node to new node
let teleportZoneFromExistingToNew = existingNode.TeleportZones.getValue(nodeInstance.Key);
if (teleportZoneFromExistingToNew == null) {
teleportZoneFromExistingToNew = new TeleportZone_1.TeleportZone(existingNode, TeleportZoneDirection_1.TeleportZoneDirection.South, currentPoint, currentPoint, existingNode.TeleportZones.size(), nodeInstance);
existingNode.TeleportZones.setValue(nodeInstance.Key, teleportZoneFromExistingToNew);
}
else {
teleportZoneFromExistingToNew.BottomRightCoordinate = currentPoint;
}
// Create/Update teleport node in new node to existing node
let teleportZoneFromNewToExisting = nodeInstance.TeleportZones.getValue(existingNode.Key);
let pointInNewNode = new Point_1.Point(currentPoint.X, currentPoint.Y + 1);
if (teleportZoneFromNewToExisting == null) {
teleportZoneFromNewToExisting = new TeleportZone_1.TeleportZone(nodeInstance, TeleportZoneDirection_1.TeleportZoneDirection.North, pointInNewNode, pointInNewNode, nodeInstance.TeleportZones.size(), existingNode);
nodeInstance.TeleportZones.setValue(existingNode.Key, teleportZoneFromNewToExisting);
}
else {
teleportZoneFromNewToExisting.BottomRightCoordinate = currentPoint;
}
}
}
// check south points
y = topLeftCoordinate.Y + nodeInstance.Height + 1;
for (let x = topLeftCoordinate.X; x < topLeftCoordinate.X + nodeInstance.Width; x++) {
let currentPoint = new Point_1.Point(x, y);
let existingNode = this._nodes.getValue(currentPoint);
// found a node - set up teleport zone
if (existingNode != null) {
// Create/Update teleport node in existing node to new node
let teleportZoneFromExistingToNew = existingNode.TeleportZones.getValue(nodeInstance.Key);
if (teleportZoneFromExistingToNew == null) {
teleportZoneFromExistingToNew = new TeleportZone_1.TeleportZone(existingNode, TeleportZoneDirection_1.TeleportZoneDirection.North, currentPoint, currentPoint, existingNode.TeleportZones.size(), nodeInstance);
existingNode.TeleportZones.setValue(nodeInstance.Key, teleportZoneFromExistingToNew);
}
else {
teleportZoneFromExistingToNew.BottomRightCoordinate = currentPoint;
}
// Create/Update teleport node in new node to existing node
let teleportZoneFromNewToExisting = nodeInstance.TeleportZones.getValue(existingNode.Key);
let pointInNewNode = new Point_1.Point(currentPoint.X, currentPoint.Y - 1);
if (teleportZoneFromNewToExisting == null) {
teleportZoneFromNewToExisting = new TeleportZone_1.TeleportZone(nodeInstance, TeleportZoneDirection_1.TeleportZoneDirection.South, pointInNewNode, pointInNewNode, nodeInstance.TeleportZones.size(), existingNode);
nodeInstance.TeleportZones.setValue(existingNode.Key, teleportZoneFromNewToExisting);
}
else {
teleportZoneFromNewToExisting.BottomRightCoordinate = currentPoint;
}
}
}
// check west points
let x = topLeftCoordinate.X - 1;
for (let y = topLeftCoordinate.Y; y < topLeftCoordinate.Y + nodeInstance.Height; y++) {
let currentPoint = new Point_1.Point(x, y);
let existingNode = this._nodes.getValue(currentPoint);
// found a node - set up teleport zone
if (existingNode != null) {
// Create/Update teleport node in existing node to new node
let teleportZoneFromExistingToNew = existingNode.TeleportZones.getValue(nodeInstance.Key);
if (teleportZoneFromExistingToNew == null) {
teleportZoneFromExistingToNew = new TeleportZone_1.TeleportZone(existingNode, TeleportZoneDirection_1.TeleportZoneDirection.East, currentPoint, currentPoint, existingNode.TeleportZones.size(), nodeInstance);
existingNode.TeleportZones.setValue(nodeInstance.Key, teleportZoneFromExistingToNew);
}
else {
teleportZoneFromExistingToNew.BottomRightCoordinate = currentPoint;
}
// Create/Update teleport node in new node to existing node
let teleportZoneFromNewToExisting = nodeInstance.TeleportZones.getValue(existingNode.Key);
let pointInNewNode = new Point_1.Point(currentPoint.X + 1, currentPoint.Y);
if (teleportZoneFromNewToExisting == null) {
teleportZoneFromNewToExisting = new TeleportZone_1.TeleportZone(nodeInstance, TeleportZoneDirection_1.TeleportZoneDirection.West, pointInNewNode, pointInNewNode, nodeInstance.TeleportZones.size(), existingNode);
nodeInstance.TeleportZones.setValue(existingNode.Key, teleportZoneFromNewToExisting);
}
else {
teleportZoneFromNewToExisting.BottomRightCoordinate = currentPoint;
}
}
}
// check east points
x = topLeftCoordinate.X + nodeInstance.Width + 1;
for (let y = topLeftCoordinate.Y; y < topLeftCoordinate.Y + nodeInstance.Height; y++) {
let currentPoint = new Point_1.Point(x, y);
let existingNode = this._nodes.getValue(currentPoint);
// found a node - set up teleport zone
if (existingNode != null) {
// Create/Update teleport node in existing node to new node
let teleportZoneFromExistingToNew = existingNode.TeleportZones.getValue(nodeInstance.Key);
if (teleportZoneFromExistingToNew == null) {
teleportZoneFromExistingToNew = new TeleportZone_1.TeleportZone(existingNode, TeleportZoneDirection_1.TeleportZoneDirection.West, currentPoint, currentPoint, existingNode.TeleportZones.size(), nodeInstance);
existingNode.TeleportZones.setValue(nodeInstance.Key, teleportZoneFromExistingToNew);
}
else {
teleportZoneFromExistingToNew.BottomRightCoordinate = currentPoint;
}
// Create/Update teleport node in new node to existing node
let teleportZoneFromNewToExisting = nodeInstance.TeleportZones.getValue(existingNode.Key);
let pointInNewNode = new Point_1.Point(currentPoint.X - 1, currentPoint.Y);
if (teleportZoneFromNewToExisting == null) {
teleportZoneFromNewToExisting = new TeleportZone_1.TeleportZone(nodeInstance, TeleportZoneDirection_1.TeleportZoneDirection.East, pointInNewNode, pointInNewNode, nodeInstance.TeleportZones.size(), existingNode);
nodeInstance.TeleportZones.setValue(existingNode.Key, teleportZoneFromNewToExisting);
}
else {
teleportZoneFromNewToExisting.BottomRightCoordinate = currentPoint;
}
}
}
//#endregion
this._nodes.setValue(topLeftCoordinate, nodeInstance);
this._minX = Math.min(this._minX, topLeftCoordinate.X);
this._minY = Math.min(this._minY, topLeftCoordinate.Y);
this._maxX = Math.max(this._maxX, topLeftCoordinate.X + nodeInstance.Width);
this._maxY = Math.max(this._maxY, topLeftCoordinate.Y + nodeInstance.Height);
}
get Width() {
return this._maxX - this._minX;
}
get Height() {
return this._maxY - this._minY;
}
get TopLeftCoordinate() {
return new Point_1.Point(this._minX, this._minY);
}
get BottomRightCoordinate() {
return new Point_1.Point(this._maxX, this._maxY);
}
}
exports.Grid = Grid;
//# sourceMappingURL=Grid.js.map
-10
View File
@@ -1,10 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//TODO: Make generic
async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
}
exports.asyncForEach = asyncForEach;
//# sourceMappingURL=Helpers.js.map
-3
View File
@@ -1,3 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=IApiSlaves.js.map
-3
View File
@@ -1,3 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=IClusterioMasterProxy.js.map
-27
View File
@@ -1,27 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
console.log("started.");
const ClusterioMasterProxy_1 = require("./ClusterioMasterProxy");
const ClusterManager_1 = require("./ClusterManager");
const argv = require('minimist')(process.argv.slice(2));
let configPath = argv['configPath'];
if (configPath === undefined || configPath === null || configPath === "") {
throw new Error("configPath is null.");
}
;
let config = require(configPath);
let proxy = new ClusterioMasterProxy_1.ClusterioMasterProxy(config.masterIP, config.masterPort, config.masterAuthToken);
let manager = new ClusterManager_1.ClusterManager();
manager.GenerateBasicGridOfNodes(2, 2, 'GridLockTest_');
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// async function test() {
// await asyncForEach([1,2,3], async (v, i, a) => {
// console.log("test" + i);
// await sleep(5000);
// });
// }
// test();
proxy.CreateNodeInstancesOnLocalServer(manager.Grid.GetNodes());
//# sourceMappingURL=ModelController.js.map
-24
View File
@@ -1,24 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
console.log("started.");
const ClusterioMasterProxy_1 = require("./ClusterioMasterProxy");
const ClusterManager_1 = require("./ClusterManager");
const argv = require('minimist')(process.argv.slice(2));
let configPath = argv['configPath'];
if (configPath === undefined || configPath === null || configPath === "") {
throw new Error("configPath is null.");
}
;
let config = require(configPath);
let proxy = new ClusterioMasterProxy_1.ClusterioMasterProxy(config.masterIP, config.masterPort, config.masterAuthToken);
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// main
(async () => {
let manager = new ClusterManager_1.ClusterManager();
manager.GenerateBasicGridOfNodes(2, 2, 'GridLockTest');
await proxy.CreateNodeInstancesOnLocalServer(manager.Grid.GetNodes());
return;
})();
//# sourceMappingURL=ModelDemo.js.map
-61
View File
@@ -1,61 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
console.log("Started.");
const ClusterioMasterProxy_1 = require("./ClusterioMasterProxy");
const ClusterManager_1 = require("./ClusterManager");
const argv = require('minimist')(process.argv.slice(2));
// arg: configPath
// description: should point to the clusterio config.json file.
let configPath = argv['configPath'];
if (configPath === undefined || configPath === null || configPath === "") {
configPath = "../../config.json";
}
;
let config = require(configPath);
// arg: mode
// description: should be GenerateBasicGridOfNodes for now. Can extend in future.
let mode = argv['mode'];
if (mode === undefined || mode === null || mode === "") {
throw new Error("mode is null. mode must be 'GenerateBasicGridOfNodes' (for now).");
}
;
// arg: serviceHostType
// description: what program to use to host the client.js instances.
let serviceHostType = argv['serviceHostType'];
if (serviceHostType === undefined || serviceHostType === null || serviceHostType === "") {
throw new Error("serviceHostType is null. serviceHostType must be either 'pm2' or 'screen'.");
}
;
if (serviceHostType != "pm2" && serviceHostType != "screen") {
throw new Error("serviceHostType must be either 'pm2' or 'screen'.");
}
// arg: width
// description: width of node grid
let width = argv['width'];
if (width === undefined || width === null || width === "") {
throw new Error("width is null. width must be an int > 0.");
}
;
// arg: height
// description: height of node grid
let height = argv['height'];
if (height === undefined || height === null || height === "") {
throw new Error("height is null. height must be an int > 0.");
}
;
// arg: namePrefix
// description: namePrefix of node grid
let namePrefix = argv['namePrefix'];
if (namePrefix === undefined || namePrefix === null || namePrefix === "") {
throw new Error("namePrefix is null.");
}
;
let proxy = new ClusterioMasterProxy_1.ClusterioMasterProxy(config.masterIP, config.masterPort, config.masterAuthToken, serviceHostType);
// main
(async () => {
let manager = new ClusterManager_1.ClusterManager();
manager.GenerateBasicGridOfNodes(width, height, namePrefix);
await proxy.CreateNodeInstancesOnLocalServer(manager.Grid.GetNodes());
return;
})();
//# sourceMappingURL=ModelRunner.js.map
-29
View File
@@ -1,29 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const Point_1 = require("./Point");
const typescript_collections_1 = require("typescript-collections");
/**
* An instance of a Factorio server. Multiple nodes can run on the same server.
*/
class NodeInstance {
constructor(name, topLeftCoordinate, width, height) {
this.TeleportZones = new typescript_collections_1.Dictionary();
this.Name = name;
this.Width = width;
this.Height = height;
this.TopLeftCoordinate = topLeftCoordinate;
}
get BottomRightCoordinate() {
return new Point_1.Point(this.TopLeftCoordinate.X + this.Width, this.TopLeftCoordinate.Y + this.Height);
}
// public AddTeleportZone(name: string, topLeftCoordinate: Point, bottomRightCoordinate: Point) {
// this.TeleportZones.push(new TeleportZone(this, topLeftCoordinate, bottomRightCoordinate, name));
// }
AddTeleportZonePoint() {
}
get Key() {
return this.Name;
}
}
exports.NodeInstance = NodeInstance;
//# sourceMappingURL=NodeInstance.js.map
-57
View File
@@ -1,57 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
class Point {
/**
* Constructs a Point. Will not accept NaNs.
* @param x A number. Cannot be NaN.
* @param y A number. Cannot be NaN.
*/
constructor(x, y) {
this.toString = () => {
return "(" + this.X + "," + this.Y + ")";
};
if (Number.isNaN(x)) {
throw new Error("x is NaN. Will not construct Point.");
}
if (Number.isNaN(y)) {
throw new Error("y is NaN. Will not construct Point.");
}
this.X = x;
this.Y = y;
}
/**
* Constructs a Point from a string representation in the format (x,y). Will not accept NaNs.
* @param vectorAsString A string in the format (x,y). Will not accept NaNs.
*/
static FromString(vectorAsString) {
this.CheckChar('(', vectorAsString[0], vectorAsString);
this.CheckChar(')', vectorAsString[vectorAsString.length - 1], vectorAsString);
if (vectorAsString.indexOf(',') === -1) {
throw new Error("vectorAsString is an invalid format. Cannot find ','. fullString: " + vectorAsString);
}
let splitStr = vectorAsString.substr(1, vectorAsString.length - 2).split(',');
if (splitStr.length != 2) {
throw new Error("vectorAsString is an invalid format. Expecting string in format (x,y). fullString: " + vectorAsString);
}
let x = parseInt(splitStr[0]);
let y = parseInt(splitStr[1]);
let v = new Point(x, y);
return v;
}
static CheckChar(expectedChar, actualChar, fullString) {
if (actualChar != expectedChar) {
throw new Error("vectorAsString is an invalid format. Expecting: '" + expectedChar + "'. Actual: '" + actualChar + "'. fullString: " + fullString);
}
}
equals(obj) {
return this.X === obj.X && this.Y === obj.Y;
}
Add(p) {
return new Point(this.X + p.X, this.Y + p.Y);
}
Subtract(p) {
return new Point(this.X - p.X, this.Y - p.Y);
}
}
exports.Point = Point;
//# sourceMappingURL=Point.js.map
-16
View File
@@ -1,16 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/**
* A server. Can contain multiple nodes (Factorio server instances).
*/
class Server {
constructor(name) {
this.Nodes = new Array();
this.Name = name;
}
AddNodeInstance(node) {
this.Nodes.push(node);
}
}
exports.Server = Server;
//# sourceMappingURL=Server.js.map
-8
View File
@@ -1,8 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var ServiceHostType;
(function (ServiceHostType) {
ServiceHostType[ServiceHostType["pm2"] = 0] = "pm2";
ServiceHostType[ServiceHostType["screen"] = 1] = "screen";
})(ServiceHostType = exports.ServiceHostType || (exports.ServiceHostType = {}));
//# sourceMappingURL=ServiceHostType.js.map
-64
View File
@@ -1,64 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const Point_1 = require("./Point");
const TeleportZoneDirection_1 = require("./TeleportZoneDirection");
const Config = require("./Config");
class TeleportZone {
constructor(parentNode, direction, topLeftCoordinate, bottomRightCoordinate, index, targetNode) {
this.TopLeftCoordinate = topLeftCoordinate;
this.BottomRightCoordinate = bottomRightCoordinate;
this.ParentNode = parentNode;
this.Direction = direction;
this.Index = index;
this.TargetNode = targetNode;
}
get Name() {
return TeleportZoneDirection_1.TeleportZoneDirection[this.Direction] + " to " + this.TargetNode.Name;
}
// public AddRestriction(restrictingTeleportZone: TeleportZone) {
// this.Restrictions.push(new TeleportZoneRestriction(this, restrictingTeleportZone));
// }
/**
* Converts this teleport zone into Factorio map co-ordinates
* @returns [name, topleft.x, topleft.y, width, height, zoneIndex]
*/
GenerateTeleportZoneInFactorioCoordinates() {
// get where this zone is relative to the node itself
let topLeftRelativeCoordinate = this.TopLeftCoordinate.Subtract(this.ParentNode.TopLeftCoordinate);
// a grid coordinate point is equivalent to a 1x1 node, so bottomright = topleft.
// add (1,1) to translate from area representation to coordinates representing actual points.
let bottomRightRelativeCoordinate = this.BottomRightCoordinate
.Subtract(this.ParentNode.TopLeftCoordinate)
.Add(new Point_1.Point(1, 1));
// Factorio map is e.g. (-500,-500) to (500, 500) for a 1000x1000 node.
// We therefore need to offset our co-ordinates so that (0,0) is the center and not (width/2, height/2)
let m = Config.GridToFactorioCoordinateSystemMultiplier;
let factorioTeleportZoneTopLeft = new Point_1.Point(topLeftRelativeCoordinate.X * m - m / 2, topLeftRelativeCoordinate.Y * m - m / 2);
let factorioTeleportZoneBottomRight = new Point_1.Point(bottomRightRelativeCoordinate.X * m - m / 2, bottomRightRelativeCoordinate.Y * m - m / 2);
let zoneWidth = factorioTeleportZoneBottomRight.X - factorioTeleportZoneTopLeft.X;
let zoneHeight = factorioTeleportZoneBottomRight.Y - factorioTeleportZoneTopLeft.Y;
let tl = factorioTeleportZoneTopLeft;
let br = factorioTeleportZoneBottomRight;
let zw = Config.TeleportZoneWidth;
switch (this.Direction) {
case TeleportZoneDirection_1.TeleportZoneDirection.North:
return [this.Name, tl.X, tl.Y, zoneWidth, zw, this.Index + 1];
case TeleportZoneDirection_1.TeleportZoneDirection.West:
return [this.Name, tl.X, tl.Y, zw, zoneHeight, this.Index + 1];
case TeleportZoneDirection_1.TeleportZoneDirection.South:
return [this.Name, tl.X, br.Y - zw, zoneWidth, zw, this.Index + 1];
case TeleportZoneDirection_1.TeleportZoneDirection.East:
return [this.Name, br.X - zw, tl.Y, zw, zoneHeight, this.Index + 1];
}
}
/**
* Generates a zone restriction from this teleport zone to target zone
* @returns [zoneIndex, targetNodeName, targetZoneName]
*/
GenerateZoneRestriction() {
let targetZone = this.TargetNode.TeleportZones.getValue(this.ParentNode.Key);
return [this.Index + 1, this.TargetNode.Name, targetZone.Name];
}
}
exports.TeleportZone = TeleportZone;
//# sourceMappingURL=TeleportZone.js.map
-10
View File
@@ -1,10 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var TeleportZoneDirection;
(function (TeleportZoneDirection) {
TeleportZoneDirection[TeleportZoneDirection["North"] = 0] = "North";
TeleportZoneDirection[TeleportZoneDirection["East"] = 1] = "East";
TeleportZoneDirection[TeleportZoneDirection["South"] = 2] = "South";
TeleportZoneDirection[TeleportZoneDirection["West"] = 3] = "West";
})(TeleportZoneDirection = exports.TeleportZoneDirection || (exports.TeleportZoneDirection = {}));
//# sourceMappingURL=TeleportZoneDirection.js.map
-10
View File
@@ -1,10 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
class TeleportZoneRestriction {
constructor(parentTeleportZone, restrictingTeleportZone) {
this.ParentTeleportZone = parentTeleportZone;
this.RestrictingTeleportZone = restrictingTeleportZone;
}
}
exports.TeleportZoneRestriction = TeleportZoneRestriction;
//# sourceMappingURL=TeleportZoneRestriction.js.map
-13
View File
@@ -1,13 +0,0 @@
"use strict";
function __export(m) {
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
}
Object.defineProperty(exports, "__esModule", { value: true });
__export(require("./ClusterManager"));
__export(require("./NodeInstance"));
__export(require("./Server"));
__export(require("./TeleportZone"));
__export(require("./TeleportZoneRestriction"));
__export(require("./Point"));
__export(require("./Grid"));
//# sourceMappingURL=index.js.map
-119
View File
@@ -1,119 +0,0 @@
//Sigmoids...
function sigmoid0(x)
{
return x/(1+Math.abs(x));
}
function sigmoid1(x)
{
if (x>=0.0) {
return 1.0
} else {
return 0.0
}
}
function sigmoid2(x)
{
if (x>=0.0){
return x
} else {
return 0.0
}
}
function sigmoid3(x)
{
return x;
}
//DOSE - Doses items for item X slave Y
//Numreq - number of items that are requested for item X for slave Y
//Instore - number of items for item X in master gauge (currently)
//store_last_tick - Number of items for item X in master gauge at time of last tick
//dole - Current dole for item X
//carry - carry for item X slave Y
//prev_req - Numreq for previous request for item X slave Y
//* doleinfo_last - Object with 2 values for last "tick period": numreq - total number or requests for item, numslave - number of slaves requesting that item
//debt - debt for item X slave Y
//Returns: Array of 3 elements
//0.Number of items to give in that dose
//1.New dole for item X
//2.New carry for item X slave Y
//3.New debt
function Dose(numreq,instore,store_last_tick,dole,carry,prev_req,numreq_total_adj,debt)
{
numreq=Number(numreq);
instore=Number(instore);
var instore_adj=instore;
if (instore_adj==0) instore_adj=0.1;
var b=Math.trunc(prev_req/instore_adj*numreq);
var inp1=[numreq,instore_adj,store_last_tick/numreq,dole,carry];//0..4
var inp2=[prev_req/numreq,numreq/numreq_total_adj,store_last_tick/numreq_total_adj,prev_req/instore_adj,b,numreq_total_adj,store_last_tick,debt];//5..12
var inputs=inp1.concat(inp2);
var internal=new Array(20).fill(0);
var outputs=[0,0,0,0];
//NN: Please don't touch :), code is generated
outputs[0]=inputs[7]*0.8058582544;
outputs[0]=sigmoid2(outputs[0]);
outputs[1]=sigmoid0(outputs[1]);
outputs[2]=sigmoid0(outputs[2]);
outputs[3]=inputs[6]*0.2294657379;
outputs[3]=sigmoid0(outputs[3]);
debt=debt || 0;
if (outputs[0]>0.99) outputs[0]=1.0;
var res1=Number(outputs[0])*numreq+outputs[3]+debt;
var res_capped=Math.round(res1);
if (res_capped>numreq) res_capped=numreq;
if (res_capped>instore) res_capped=instore;
if (res_capped<0) res_capped=0;
res1=res1 || 0;
res_capped=res_capped || 0;//Safeguard against wrong values
debt=res1-res_capped;
debt_cap=Math.round(numreq*1.5)+5;
if (debt>debt_cap) debt=debt_cap;
carry=sigmoid0(outputs[2]*carry+outputs[0]) || 0;
return [res_capped,dole+outputs[1],carry,debt];
}
//TICK - Updates values for item X, should be called once for each item for each round of all slaves requesting that item
//Instore - number of items for item X in master gauge (currently)
//dole - Current dole for item X
//store_last_tick - Number of items for item X in master gauge at time of last tick (not this one)
//* doleinfo_last - Object with 2 values for last "tick period": numreq - total number or requests for item, numslave - number of slaves requesting that item
//* doleinfo_new - Object with 2 values for new "tick period": numreq - total number or requests for item, numslave - number of slaves requesting that item
//Returns: Array of 2 elements
//0.New dole for item X
//1.Guessed percentage average of items supplied in last 10 ticks
function Tick(instore,dole,store_last_tick,numreq_total_adj)
{
var instore_adj=instore;
if (instore_adj==0) instore_adj=0.1;
var inputs=[instore,dole,store_last_tick/instore_adj,store_last_tick/numreq_total_adj,store_last_tick,numreq_total_adj];
var internal=new Array(10).fill(0);
var outputs=[0,0];
//NN: Please don't touch :), code is generated
outputs[0]=inputs[3]*0.9364774823;
outputs[0]=outputs[0]*1.0*0.4590721726;
outputs[0]=sigmoid2(outputs[0]);
outputs[1]=outputs[0]*1.828422785;
outputs[1]=sigmoid3(outputs[1]);
avg=outputs[1];
if (numreq_total_adj<0.11) avg=1;//When no item's are requested, result is 0.1 ; In that case there is no deficit, so return 1 = 100% demand fulfilled
if (avg>1) avg=1;
if (avg<0) avg=0;
return [sigmoid0(outputs[0]),avg];
}
module.exports = {Dose,Tick}
+1 -1
View File
@@ -1,7 +1,7 @@
var assert = require("assert");
var fs = require("fs");
var fileOps = require("lib/fileOps");
var fileOps = require("_app/fileOps");
var generateSSLcert = require("./generateSSLcert.js")
describe("generateSSLcert.js(options)", ()=>{
+15
View File
@@ -0,0 +1,15 @@
import { getItemDatabase } from "./getItemDatabase";
import { getSlaveDatabase } from "./getSlaveDatabase";
export type db = {
items: {[name:string]: any},
slaves: Slaves,
}
export function getDatabase(config: ClusterioConfig) {
let db: db = {
items: getItemDatabase(config),
slaves: getSlaveDatabase(config),
};
return db
}
+6
View File
@@ -0,0 +1,6 @@
declare module "getFactorioLocale" {
interface callback {
(error: Error, factorioLocale?: any): void
}
export function asObject(factorioDirectory: string, languageCode: string, callback: callback): void
}
+1 -1
View File
@@ -1,4 +1,4 @@
const fileOps = require("lib/fileOps");
const fileOps = require("_app/fileOps");
// const config = require("./../config");
module.exports = function(config) {
+40
View File
@@ -0,0 +1,40 @@
import * as fs from "fs"
import * as path from "path"
export function getItemDatabase(config: ClusterioConfig): {[name:string]: number | Function} {
var items: {[name: string]: number | Function} = {}
try{
let x = fs.statSync(path.resolve(config.databaseDirectory, "items.json"));
console.log(`loading items from ${path.resolve(config.databaseDirectory, "items.json")}`);
items = JSON.parse(fs.readFileSync(path.resolve(config.databaseDirectory, "items.json")).toString());
} catch (e){
items = {};
}
items.addItem = function addItem(object: item): boolean {
if(object.name == "addItem" || object.name == "removeItem") {
console.error("Fuck you, that would screw everything up if you named your item that.");
return false;
} else {
if(this[object.name] && Number(this[object.name]) != NaN){
this[object.name] = Number(this[object.name]) + Number(object.count);
} else {
this[object.name] = object.count;
}
return true;
}
},
items.removeItem = function removeItem(object: item): boolean {
if(object.name == "addItem" || object.name == "removeItem") {
console.error("Fuck you, that would screw everything up if you named your item that.");
return false;
} else {
if(this[object.name] && Number(this[object.name]) != NaN){
this[object.name] = Number(this[object.name]) - Number(object.count);
} else {
this[object.name] = 0;
}
return true;
}
}
return items
}
+14
View File
@@ -0,0 +1,14 @@
import * as fs from "fs"
import * as path from "path"
export function getSlaveDatabase(config: ClusterioConfig): Slaves {
var slaves: Slaves = {};
try{
let x = fs.statSync(path.resolve(config.databaseDirectory, "/slaves.json"));
console.log(`loading slaves from path.resolve(config.databaseDirectory, "slaves.json")`);
slaves = JSON.parse(fs.readFileSync(path.resolve(config.databaseDirectory, "slaves.json"), "utf-8"));
} catch (e){
slaves = {};
}
return slaves
}
-22
View File
@@ -1,22 +0,0 @@
const crypto = require('crypto');
const fs = require('fs');
/**
* Returns a promise that resolves to the SHA1 hash of the file given by path
*/
exports.hashFile = function(path) {
return new Promise(function(resolve, reject) {
let hasher = crypto.createHash('sha1')
hasher.setEncoding('hex')
hasher.on('finish', function() {
resolve(hasher.read());
});
let stream = fs.createReadStream(path);
stream.on('error', function(error) {
// the docs doesn't say anything about what's passed on error
reject(error);
});
stream.pipe(hasher);
});
}
-2
View File
@@ -1,2 +0,0 @@
const fs = require("fs-extra");
const needle = require("needle");
+2 -1
View File
@@ -3,6 +3,7 @@ Provides tools to manage mods in a clusterio install
@module modManager
*/
const rightpad = require("right-pad");
const asTable = require("as-table").configure({delimiter: ' | '});
const moment = require("moment");
const syncRequest = require("sync-request");
@@ -12,7 +13,7 @@ const factorioAPI = require("node-factorio-api");
const sLog = require("single-line-log").stdout;
const query = require('cli-interact').getYesNo;
const fileOps = require("lib/fileOps");
const fileOps = require("_app/fileOps");
// const config = require("./../../config");
+1
View File
@@ -56,6 +56,7 @@ function download(url, file, callback){
}
var directories = [
"database/linvodb",
"sharedMods",
"instances",
];
-5
View File
@@ -1,5 +0,0 @@
{
"name": "lib",
"description": "Internal library for factorioClusterio"
}
+30 -75
View File
@@ -35,99 +35,54 @@ require 'hotpatch.gui'
-- As this is part of the base mod, the developers have given permission for modders to use/adapt the assets therein, for modding Factorio
-- Any other use would require licensing/permission from Wube
-- If you don't want to run this, either comment this out before creating your scenario, OR uninstall freeplay at runtime
hotpatch_tools.static_mod('freeplay', '0.17.50', [===[
local util = require("util")
hotpatch_tools.static_mod('freeplay', '1.0.0', [===[
local silo_script = require("silo-script")
local created_items = function()
return
{
["iron-plate"] = 8,
["wood"] = 1,
["pistol"] = 1,
["firearm-magazine"] = 10,
["burner-mining-drill"] = 1,
["stone-furnace"] = 1
}
end
local respawn_items = function()
return
{
["pistol"] = 1,
["firearm-magazine"] = 10
}
end
for k,v in pairs(silo_script.get_events()) do
script.on_event(k, v)
end
local version = 1
script.on_event(defines.events.on_player_created, function(event)
local player = game.players[event.player_index]
util.insert_safe(player, global.created_items)
local r = global.chart_distance or 200
player.force.chart(player.surface, {{player.position.x - r, player.position.y - r}, {player.position.x + r, player.position.y + r}})
if not global.skip_intro then
if game.is_multiplayer() then
player.print({"msg-intro"})
else
game.show_message_dialog{text = {"msg-intro"}}
end
player.insert{name="iron-plate", count=8}
player.insert{name="pistol", count=1}
player.insert{name="firearm-magazine", count=10}
player.insert{name="burner-mining-drill", count = 1}
player.insert{name="stone-furnace", count = 1}
player.force.chart(player.surface, {{player.position.x - 200, player.position.y - 200}, {player.position.x + 200, player.position.y + 200}})
if (#game.players <= 1) then
game.show_message_dialog{text = {"msg-intro"}}
else
player.print({"msg-intro"})
end
silo_script.on_event(event)
silo_script.on_player_created(event)
end)
script.on_event(defines.events.on_player_respawned, function(event)
local player = game.players[event.player_index]
util.insert_safe(player, global.respawn_items)
silo_script.on_event(event)
player.insert{name="pistol", count=1}
player.insert{name="firearm-magazine", count=10}
end)
script.on_configuration_changed(function(event)
global.created_items = global.created_items or created_items()
global.respawn_items = global.respawn_items or respawn_items()
silo_script.on_configuration_changed(event)
end)
script.on_load(function()
silo_script.on_load()
script.on_event(defines.events.on_gui_click, function(event)
silo_script.on_gui_click(event)
end)
script.on_init(function()
global.created_items = created_items()
global.respawn_items = respawn_items()
global.version = version
silo_script.on_init()
end)
script.on_event(defines.events.on_rocket_launched, function(event)
silo_script.on_rocket_launched(event)
end)
script.on_configuration_changed(function(event)
if global.version ~= version then
global.version = version
end
silo_script.on_configuration_changed(event)
end)
silo_script.add_remote_interface()
silo_script.add_commands()
remote.add_interface("freeplay",
{
get_created_items = function()
return global.created_items
end,
set_created_items = function(map)
global.created_items = map
end,
get_respawn_items = function()
return global.respawn_items
end,
set_respawn_items = function(map)
global.respawn_items = map
end,
set_skip_intro = function(bool)
global.skip_intro = bool
end,
set_chart_distance = function(value)
global.chart_distance = tonumber(value)
end
})
]===])
-- Multi-file test
@@ -143,7 +98,7 @@ files['testfolder.test'] = [===[
]===]
files['test'] = [===[
require 'testfolder.test'
require 'testfolder.test'
]===]
hotpatch_tools.static_mod('require-test', '1.0.0', [===[
+6 -6
View File
@@ -1,5 +1,5 @@
local hotpatch_tools = require 'hotpatch.mod-tools'
hotpatch_tools.static_mod('hotpatch-commands', '1.0.2', [===[
hotpatch_tools.static_mod('hotpatch-commands', '1.0.1', [===[
--[[
Copyright 2018 Chrisgbk
@@ -61,11 +61,11 @@ sub_commands = {
if (not param) or (param == '') then
caller.print('Usage: /hotpatch [command] [parameters]')
caller.print('Available commands:')
for k, _ in pairs(sub_commands) do caller.print(k) end
for k, v in pairs(sub_commands) do caller.print(k) end
caller.print('Use /hotpatch help [command] for help on a particular command')
else
local text = help_commands[param]
if text then
if text then
caller.print(text)
end
end
@@ -82,7 +82,7 @@ sub_commands = {
caller.print(table.concat{'[', k, '] ', v.name, ' ', v.version, ' ', (v.loaded and 'loaded') or 'not loaded', ' ', (v.running and 'running') or 'not running'})
end
else
end
end,
}
@@ -113,7 +113,7 @@ local admin_commands = {
_ENV.commands.add_command('hotpatch', 'Commands for hotpatch. Run /hotpatch help for details.', function(e)
local caller = (e.player_index and game.players[e.player_index]) or console
if (not e.parameter) or (e.parameter == '') then
if (not e.parameter) or (e.parameter == '') then
sub_commands['help'](e.player_index)
return
end
@@ -130,6 +130,6 @@ _ENV.commands.add_command('hotpatch', 'Commands for hotpatch. Run /hotpatch help
end
end)
--]===])
]===])
return true
+85 -87
View File
@@ -1,5 +1,5 @@
local hotpatch_tools = require 'hotpatch.mod-tools'
hotpatch_tools.static_mod('hotpatch-gui', '1.0.6', [===[
hotpatch_tools.static_mod('hotpatch-gui', '1.0.4', [===[
--[[
Copyright 2018 Chrisgbk
@@ -55,26 +55,26 @@ script.on_event(defines.events.on_player_joined_game, function(e)
local top = mod_gui.get_button_flow(player)
local left = mod_gui.get_frame_flow(player)
local center = player.gui.center
local button = top['hotpatch-button']
if button then
button.destroy()
end
local menu = left['hotpatch-menu']
if menu then
if menu then
menu.destroy()
end
local IDE = center['hotpatch-IDE']
if IDE then
if IDE then
IDE.destroy()
end
local main = center['hotpatch-main']
if main then
if main then
main.destroy()
end
button = top.add{type = 'sprite-button', name = 'hotpatch-button', sprite='utility/heat_exchange_indication', tooltip = 'Hotpatch', style = mod_gui.button_style}
end)
@@ -86,33 +86,37 @@ on_gui_click_handlers = {
--on_gui_click_handlers['hotpatch-menu.IDE'](e)
--do return end
local player = game.players[e.player_index]
local top = mod_gui.get_button_flow(player)
local left = mod_gui.get_frame_flow(player)
local center = player.gui.center
local menu = left['hotpatch-menu']
if not menu then
menu = left.add{type = 'frame', name = 'hotpatch-menu', direction = 'vertical'}
menu.add{type = 'button', name = 'hotpatch-menu.IDE', caption = 'Mod IDE', tooltip = 'Open debugging GUI'}
menu.add{type = 'button', name = 'hotpatch-menu.console', caption = 'Mod Console', tooltip = 'Open debugging console'}
menu.visible = true
menu.style.visible = true
return
end
menu.visible = not menu.visible
menu.style.visible = not menu.style.visible
end,
['hotpatch-menu'] = function(e)
['hotpatch-menu'] = function(e)
local player = game.players[e.player_index]
local top = mod_gui.get_button_flow(player)
local left = mod_gui.get_frame_flow(player)
if e.element.name ~= 'hotpatch-menu' then
local menu = left['hotpatch-menu']
menu.visible = not menu.visible
on_gui_click_handlers[e.element.name](e)
end
local center = player.gui.center
local menu = left['hotpatch-menu']
menu.style.visible = not menu.style.visible
on_gui_click_handlers[e.element.name](e)
end,
['hotpatch-menu.IDE'] = function(e)
local player = game.players[e.player_index]
local top = mod_gui.get_button_flow(player)
local left = mod_gui.get_frame_flow(player)
local center = player.gui.center
local IDE = center['hotpatch-IDE']
if not IDE then
IDE = center.add{type = 'frame', name = 'hotpatch-IDE', direction = 'vertical', caption = 'Hotpatch IDE', style = mod_gui.frame_style}
@@ -128,22 +132,22 @@ on_gui_click_handlers = {
local code = IDE_table.add{type = 'text-box', name = 'hotpatch-IDE-code'}
code.word_wrap = true
files.style.width = 400
files.style.cell_padding = 0
files.style.cell_spacing = 0
files.style.top_padding = 0
files.style.bottom_padding = 0
code.style.width = 600
code.style.height = 600
IDE.visible = false
IDE.style.visible = false
end
local IDE_dropdown = IDE['hotpatch-IDE-top']['hotpatch-IDE-mod-selector']
IDE_dropdown.clear_items()
for _, v in ipairs(installed_mods) do
for k, v in ipairs(installed_mods) do
IDE_dropdown.add_item(v.name)
end
IDE.visible = not IDE.visible
if IDE.visible then
IDE.style.visible = not IDE.style.visible
if IDE.style.visible then
player.opened = IDE
else
player.opened = nil
@@ -151,16 +155,18 @@ on_gui_click_handlers = {
end,
['hotpatch-menu.console'] = function(e)
local player = game.players[e.player_index]
local top = mod_gui.get_button_flow(player)
local left = mod_gui.get_frame_flow(player)
local center = player.gui.center
local hotpatch_console = center['hotpatch-console']
if not hotpatch_console then
hotpatch_console = center.add{type = 'frame', name = 'hotpatch-console', direction = 'vertical', caption = 'Hotpatch Console'}
local top_flow = hotpatch_console.add{type = 'flow', name = 'hotpatch-console-top', direction = 'horizontal'}
local console = center['hotpatch-console']
if not console then
console = center.add{type = 'frame', name = 'hotpatch-console', direction = 'vertical', caption = 'Hotpatch Console'}
local top_flow = console.add{type = 'flow', name = 'hotpatch-console-top', direction = 'horizontal'}
top_flow.add{type = 'label', name = 'hotpatch-console-mod-label', caption = 'Mod: '}
local console_dropdown = top_flow.add{type = 'drop-down', name = 'hotpatch-console-mod-selector'}
top_flow.add{type = 'label', name = 'hotpatch-console-mod-version', caption = 'Version: No mod selected'}
local bottom_flow = hotpatch_console.add{type = 'flow', name = 'hotpatch-console-bottom', direction = 'vertical'}
local bottom_flow = console.add{type = 'flow', name = 'hotpatch-console-bottom', direction = 'vertical'}
local output = bottom_flow.add{type = 'text-box', name = 'hotpatch-console-output'}
local bottom_input_flow = bottom_flow.add{type = 'flow', name = 'hotpatch-console-bottom-input', direction = 'horizontal'}
local input = bottom_input_flow.add{type = 'textfield', name = 'hotpatch-console-input'}
@@ -168,51 +174,53 @@ on_gui_click_handlers = {
output.style.width = 800
output.style.height = 600
input.style.width = 600
hotpatch_console.visible = false
end
local console_dropdown = hotpatch_console['hotpatch-console-top']['hotpatch-console-mod-selector']
local console_dropdown = console['hotpatch-console-top']['hotpatch-console-mod-selector']
console_dropdown.clear_items()
for _, v in ipairs(loaded_mods) do
for k, v in ipairs(loaded_mods) do
console_dropdown.add_item(v.name)
end
hotpatch_console.visible = not hotpatch_console.visible
if hotpatch_console.visible then
player.opened = hotpatch_console
console.style.visible = not console.style.visible
if console.style.visible then
player.opened = console
else
player.opened = nil
end
end,
['hotpatch-console-run'] = function(e)
local player = game.players[e.player_index]
local top = mod_gui.get_button_flow(player)
local left = mod_gui.get_frame_flow(player)
local center = player.gui.center
local hotpatch_console = center['hotpatch-console']
local output = hotpatch_console['hotpatch-console-bottom']['hotpatch-console-output']
local input = hotpatch_console['hotpatch-console-bottom']['hotpatch-console-bottom-input']['hotpatch-console-input']
local console_dropdown = hotpatch_console['hotpatch-console-top']['hotpatch-console-mod-selector']
local console = center['hotpatch-console']
local output = console['hotpatch-console-bottom']['hotpatch-console-output']
local input = console['hotpatch-console-bottom']['hotpatch-console-bottom-input']['hotpatch-console-input']
local console_dropdown = console['hotpatch-console-top']['hotpatch-console-mod-selector']
if not player.admin then
output.text = table.concat{output.text, '\n', 'Only admins!'}
return
end
if not (console_dropdown.selected_index > 0) then
output.text = table.concat{output.text, '\n', 'Select a mod!'}
return
end
local mod_name = console_dropdown.items[console_dropdown.selected_index]
local loaded_index = find_loaded_mod(mod_name)
local env = loaded_mods[loaded_index].env
local old_print = env.print
local old_log = env.log
env.print = function(...)
--local count = select('#', ...)
local t = table.pack(...)
@@ -222,32 +230,33 @@ on_gui_click_handlers = {
local text = table.concat(t, '\t')
output.text = table.concat{output.text, '\n', text}
end
env.log = function(text)
output.text = table.concat{output.text, '\n', text}
end
local success
local code, err = load(input.text, input.text, 't', env)
if code then
success, err = pcall(code)
if code then
local success, err = pcall(code)
if not success then
output.text = table.concat{output.text, '\n', err}
end
else
output.text = table.concat{output.text, '\n', err}
end
env.print = old_print
env.log = old_log
end,
['hotpatch-IDE-file'] = function(e)
local player = game.players[e.player_index]
local top = mod_gui.get_button_flow(player)
local left = mod_gui.get_frame_flow(player)
local center = player.gui.center
local element = e.element
local previous
for _, v in pairs(center['hotpatch-IDE']['hotpatch-IDE-table']['hotpatch-IDE-files']['hotpatch-IDE-files-table'].children) do
for k, v in pairs(center['hotpatch-IDE']['hotpatch-IDE-table']['hotpatch-IDE-files']['hotpatch-IDE-files-table'].children) do
if table.compare(v.style.font_color or {}, {r=1.0, g=1.0, b=0.0, a=1.0}) then
previous = v
break
@@ -285,8 +294,10 @@ end)
on_gui_selection_state_changed_handlers = {
['hotpatch-console-mod-selector'] = function(e)
local player = game.players[e.player_index]
local top = mod_gui.get_button_flow(player)
local left = mod_gui.get_frame_flow(player)
local center = player.gui.center
local element = e.element
local name = element.items[element.selected_index]
if name then
@@ -295,18 +306,20 @@ on_gui_selection_state_changed_handlers = {
local mod = loaded_mods[index]
local version_label = center['hotpatch-console']['hotpatch-console-top']['hotpatch-console-mod-version']
version_label.caption = 'Version: ' .. mod.version
local hotpatch_console = center['hotpatch-console']
local output = hotpatch_console['hotpatch-console-bottom']['hotpatch-console-output']
local console = center['hotpatch-console']
local output = console['hotpatch-console-bottom']['hotpatch-console-output']
output.text = ''
end
end
end,
['hotpatch-IDE-mod-selector'] = function(e)
local player = game.players[e.player_index]
local top = mod_gui.get_button_flow(player)
local left = mod_gui.get_frame_flow(player)
local center = player.gui.center
local element = e.element
local name = element.items[element.selected_index]
if name then
local index = find_installed_mod(name)
@@ -319,7 +332,7 @@ on_gui_selection_state_changed_handlers = {
local file = list.add{type = 'label', style = 'hoverable_bold_label', caption = 'control', name = 'hotpatch-IDE-file.control'}
file.style.bottom_padding = 0
file.style.top_padding = 0
for k, _ in pairs(mod.files) do
for k, v in pairs(mod.files) do
file = list.add{type = 'label', style = 'hoverable_bold_label', caption = k, name = 'hotpatch-IDE-file.' .. k}
file.style.bottom_padding = 0
file.style.top_padding = 0
@@ -343,35 +356,20 @@ script.on_event(defines.events.on_gui_closed, function(e)
if e.gui_type == defines.gui_type.custom then
if element and element.valid then
if element.name == 'hotpatch-IDE' then
if element.visible then
if element.style.visible then
player.play_sound{path = "utility/gui_click"}
end
element.visible = false
element.style.visible = false
elseif element.name == 'hotpatch-console' then
if element.visible then
if element.style.visible then
player.play_sound{path = "utility/gui_click"}
end
element.visible = false
element.style.visible = false
end
end
end
end)
--]===])
--[===[
-- keeping this as a reminder
['hotpatch-menu'] = function(e)
local player = game.players[e.player_index]
local top = mod_gui.get_button_flow(player)
local left = mod_gui.get_frame_flow(player)
local center = player.gui.center
local menu = left['hotpatch-menu']
menu.visible = not menu.visible
on_gui_click_handlers[e.element.name](e)
end,
--]===]
]===])
return true
+187 -199
View File
@@ -15,9 +15,6 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
log('Hotpatch runtime initializing...')
local util = require 'util'
local compat_mode = false -- enable some compatibility settings, which can help some mods load
local strict_mode = false -- causes hotpatch to hard-stop on mods doing bad things
local debug_levels = {['disabled'] = -1, ['severe'] = 1, ['error'] = 2, ['warning'] = 3, ['info'] = 4, ['verbose'] = 5, ['trace'] = 6}
-- configuration options; these should be exposed via API one day
@@ -26,15 +23,12 @@ local debug_log_to_console_only = _ENV.debug_settings.log_to_console_only
local debug_log_to_RCON = _ENV.debug_settings.log_to_RCON --only affects when log_to_console_only is in effect
local debug_log_on_tick = _ENV.debug_settings.log_on_tick
local compat_mode = false -- enable some compatibility settings, which can help some mods load
local strict_mode = false -- causes hotpatch to hard-stop on mods doing bad things
-- convenience object(rcon.print also prints to stdout when called from server console)
local console = {name = 'Console', admin = true, print = function(...) rcon.print(...) end, color = {1,1,1,1}}
-- mapping of events to names for logging
local event_names = {}
for k,v in pairs(defines.events) do
event_names[v] = k
end
-- these represent the mods as statically packaged with the scenario
local static_mods = {}
-- static_mods[i] = {}
@@ -73,7 +67,7 @@ local mod_obj_template = {
}
-- this is for the core factorio libraries; hotpatch MUST load these now, as they cannot be dynamically loaded due to factorio limitation
local loaded_libraries = {}
local loaded_libraries = {}
-- mod installation/uninstallation support functions
-- These take a mod NAME as a first argument
@@ -107,8 +101,6 @@ local mod_on_configuration_changed
local on_event
local on_nth_tick
local register_all_events
-- core handlers
local on_init -- installs static mods, loads, runs, and calls mod_on_init
local on_load -- loads, runs, and calls mod_on_load
@@ -133,6 +125,12 @@ local function static_mod(name, version, code, files)
table.insert(static_mods, mod)
end
-- mapping of events to names for logging
local event_names = {}
for k,v in pairs(defines.events) do
event_names[v] = k
end
-- This scenario contains an embedded locale, but it's unavailable until this file finishes loading
-- ie: calls to log{...} will fail right now, but will work in events
-- Rseding91 fixed it for 0.17 (https://forums.factorio.com/viewtopic.php?f=23&t=60767)
@@ -224,12 +222,12 @@ local function build_locale(ini)
local t = {}
local section = ''
local temp
-- line must end with a linefeed - single line file with no LF will fail
if not ini:match('[\r\n]+$') then
ini = ini .. '\n'
end
-- for each non-empty line do
local key, value
for l in ini:gmatch('[\r\n]*(.-)[\r\n]+') do
@@ -263,7 +261,7 @@ local function static_translate(t, recursive)
-- only translate tables that have a string as the first item
local k = t[1]
if type(k) ~= 'string' then return t end
-- make a copy, don't destroy the original table, after we copy we can translate in place
if not recursive then
t = table.deepcopy(t)
@@ -287,12 +285,12 @@ local function static_translate(t, recursive)
-- substitution of parameters: use literal value of parameter n
-- __n__
local result = (pattern:gsub('__(%d+)__', function(s) return tostring(t[tonumber(s)+1]) end))
-- re-substitution engine: match value of parameter n to provide additional translation; use for pluralization
-- __n:(pattern-1)=substitution-1;(pattern-2)=substitution-2;...(pattern-i)=substitution-i;__
for n, p in result:gmatch('__(%d+)(:.-;)__') do
for x, y in p:gmatch('%((.-)%)=(.-[^%%]);') do
if t[tonumber(n)+1]:match(x) then
if t[tonumber(n)+1]:match(x) then
result = result:gsub(table.concat{'__', n, escape(p), '__'}, unescape(y))
break
end
@@ -326,7 +324,7 @@ local hidden_log = hidden(static_translate)
-- logs localized data
local function debug_log(message, mod_name, stack_level)
if debug_level > -1 then
if debug_level > -1 then
if not stack_level then stack_level = 2 end
local di = debug.getinfo(stack_level)
local line = di.currentline
@@ -334,7 +332,7 @@ local function debug_log(message, mod_name, stack_level)
local class = 'hotpatch.info'
local log_type = (mod_name and 'hotpatch.log-mod') or 'hotpatch.log'
local severity
if type(message) == 'table' then
if type(message) == 'table' then
severity = message[1]:match('.-%-([^%-]*)%.')
if not severity then
severity = message[1]:match('.-%-.-%-(.*)%.')
@@ -346,7 +344,7 @@ local function debug_log(message, mod_name, stack_level)
else
severity = 'always'
end
local level = ((severity == 'always') and 0) or debug_levels[severity]
local level = ((severity == 'always') and 0) or debug_levels[severity]
if debug_level >= level then
if debug_log_to_console_only then
if debug_log_to_RCON then
@@ -365,15 +363,15 @@ debug_log({'hotpatch-info.logging-enabled'})
-- track _ENV accesses
setmetatable(_ENV, {
__index = function(_, k)
debug_log({'hotpatch-trace.nil-var-access', k}, nil, 3)
return nil
end,
__newindex = function(t, k, v)
debug_log({'hotpatch-trace.nil-var-assignment', k}, nil, 3)
rawset(t,k,v)
end,
__metatable = false
__index = function(t, k)
debug_log({'hotpatch-trace.nil-var-access', k}, nil, 3)
return nil
end,
__newindex = function(t, k, v)
debug_log({'hotpatch-trace.nil-var-assignment', k}, nil, 3)
rawset(t,k,v)
end,
__metatable = false
})
debug_log({'hotpatch-info.metatable-installed'})
@@ -384,6 +382,7 @@ debug_log({'hotpatch-info.loading-libs'})
local libraries = {
'camera',
'flying_tags',
'inspect',
'math3d',
'mod-gui',
'noise',
@@ -395,7 +394,7 @@ local libraries = {
for k, v in pairs(libraries) do
debug_log({'hotpatch-info.loading-library', v})
loaded_libraries[k] = require(v)
loaded_libraries[k] = require(v)
end
debug_log({'hotpatch-info.complete', {'hotpatch-info.loading-libs'}})
@@ -430,14 +429,14 @@ uninstall_mod = function(mod_name)
return
end
local loaded_index = find_loaded_mod(mod_name)
if loaded_index then
unload_mod(loaded_index)
end
debug_log({'hotpatch-info.uninstalling', mod_name})
table.remove(global.mods, index)
end
@@ -448,7 +447,7 @@ install_mod_file = function(mod_name, mod_file, mod_file_code)
return
end
local mod = global.mods[index]
debug_log({'hotpatch-info.installing-file', mod_file}, mod_name)
mod_file = mod_file:gsub('/', '.')
@@ -476,7 +475,7 @@ install_mod = function(mod_name, mod_version, mod_code, mod_files)
end
debug_log({'hotpatch-warning.contains-comments-console'}, mod_name)
end
mod_code = mod_code:gsub('\t', ' ')
mod.name = mod_name
@@ -484,7 +483,7 @@ install_mod = function(mod_name, mod_version, mod_code, mod_files)
mod.code = mod_code
mod.version = mod_version
mod.global = mod.global or {}
if mod_files then
for k,v in pairs(mod_files) do
install_mod_file(mod_name, k, v)
@@ -502,7 +501,7 @@ load_mod = function(installed_index)
--TODO notify that mod was already loaded
unload_mod(loaded_index)
end
local mod_obj = {
name = mod_name, -- name of the current mod
version = mod.version,
@@ -517,7 +516,7 @@ load_mod = function(installed_index)
on_nth_tick = {}, -- list of on_nth_tick handlers registered
loaded_files = {}, -- list of files loaded by require() that were installed into the virtual file system
}
debug_log({'hotpatch-info.script-shim'}, mod_name)
--mods private script table/shim
local mod_script = {}
@@ -542,7 +541,7 @@ load_mod = function(installed_index)
mod_script.on_nth_tick = function(tick, f)
if tick then
if type(tick) == 'table' then
for _, v in pairs(tick) do
for k, v in pairs(tick) do
mod_script.on_nth_tick(v, f)
end
return
@@ -552,12 +551,12 @@ load_mod = function(installed_index)
mod_obj.on_nth_tick = {}
end
end
else
else
mod_script.on_event = function(event, f)
if event == defines.events.on_tick then
mod_obj.on_tick = f
if mod_obj.running then
register_on_tick(mod_name)
register_on_tick(mod_name, event)
end
else
mod_obj.on_event[event] = f
@@ -569,7 +568,7 @@ load_mod = function(installed_index)
mod_script.on_nth_tick = function(tick, f)
if tick then
if type(tick) == 'table' then
for _, v in pairs(tick) do
for k, v in pairs(tick) do
mod_script.on_nth_tick(v, f)
end
return
@@ -579,11 +578,11 @@ load_mod = function(installed_index)
register_nth_tick(mod_name, tick)
end
else
local mod_on_nth_tick = mod_obj.on_nth_tick
local on_nth_tick = mod_obj.on_nth_tick
mod_obj.on_nth_tick = {}
if mod_obj.running then
for _, v in pairs(mod_on_nth_tick) do
register_nth_tick(mod_name, v)
for k, v in pairs(on_nth_tick) do
register_nth_tick(mod_name, tick)
end
end
end
@@ -605,7 +604,7 @@ load_mod = function(installed_index)
mod_script.mod_name = function()
return script.mod_name()
end
debug_log({'hotpatch-info.setting-env'}, mod_name)
-- mods private env
local env = mod_obj.env
@@ -614,28 +613,28 @@ load_mod = function(installed_index)
-- mods private package.loaded
local loaded = {}
-- copy the current environment
for k,v in pairs(_ENV) do
env[k] = v
end
for k,v in pairs(_ENV) do
env[k] = v
end
-- copy package.loaded
for k,v in pairs(_ENV.package.loaded) do
loaded[k] = v
end
loaded[k] = v
end
loaded._G = env
-- so many ways to escape sandboxes...
for k,v in pairs(_ENV.package) do
pack[k] = v
end
pack[k] = v
end
pack.loaded = loaded
env.package = pack
loaded.package = pack
env.script = mod_script
env.global = mod.global
env._G = env
env['remote'] = {
add_interface = function(name, functions)
if remote.interfaces[name] then
@@ -651,7 +650,7 @@ load_mod = function(installed_index)
return remote.call(...)
end,
interfaces = setmetatable({}, {
__index = function(_, k) return remote.interfaces[k] end,
__index = function(t, k) return remote.interfaces[k] end,
__pairs = function(t) local function iter(t, k) local v; k, v = next(remote.interfaces, k); if v then return k, t[k] end; end; return iter, t, nil end
})
}
@@ -667,29 +666,29 @@ load_mod = function(installed_index)
return commands.remove_command(name)
end,
commands = setmetatable({}, {
__index = function(_, k) return commands.commands[k] end,
__index = function(t, k) return commands.commands[k] end,
__pairs = function(t) local function iter(t, k) local v; k, v = next(commands.commands, k); if v then return k, t[k] end; end; return iter, t, nil end
}),
game_commands = setmetatable({}, {
__index = function(_, k) return commands.game_commands[k] end,
__index = function(t, k) return commands.game_commands[k] end,
__pairs = function(t) local function iter(t, k) local v; k, v = next(commands.game_commands, k); if v then return k, t[k] end; end; return iter, t, nil end
})
}
env.require = function(path)
-- I blame Nexela for this
path = path:gsub('/', '.')
path = path:gsub('\\', '.')
local alt_path = ''
local alt_path = ''
if env.package._current_path_in_package then
alt_path = env.package._current_path_in_package .. path
end
if mod_obj.loaded_files[path] then
debug_log({'hotpatch-trace.cached-load-require', path}, mod_name)
return mod_obj.loaded_files[path]
elseif mod_obj.loaded_files[alt_path] then
debug_log({'hotpatch-trace.cached-load-require', alt_path}, mod_name)
elseif mod_obj.loaded_files[alt_path] then
debug_log({'hotpatch-trace.cached-load-require', alt_path}, mod_name)
return mod_obj.loaded_files[alt_path]
else
local oldbase = env.package._current_path_in_package
@@ -711,7 +710,7 @@ load_mod = function(installed_index)
error(err)
end
end
file = global.mods[installed_index].files[alt_path]
local file = global.mods[installed_index].files[alt_path]
if file then
debug_log({'hotpatch-trace.load-require', alt_path}, mod_name)
local code, err = load(file, '[' .. mod_name .. '] ' .. alt_path .. '.lua', 'bt', env)
@@ -725,31 +724,31 @@ load_mod = function(installed_index)
error(err)
end
end
debug_log({'hotpatch-trace.load-core-lib', path}, mod_name)
env.package._current_path_in_package = oldbase
local lib = package.loaded[path]
if not lib then
debug_log({'hotpatch-error.path-not-found', path}, mod_name, 3)
local lib = package.loaded[path]
if not lib then
debug_log(path .. ' not found', nil, 3)
error(path .. ' not found')
end
end
return lib
end
end
env['load'] = function(l, s, m, e)
return load(l, s, m, e or env)
end
env['loadstring'] = env['load']
env['game'] = setmetatable({}, {
__index = function(_, k) return game[k] end,
__index = function(t, k) return game[k] end,
__pairs = function(t) local function iter(t, k) local v; k, v = next(game, k); if v then return k, t[k] end; end; return iter, t, nil end
})
local mt = {}
mt.__index = function(_, k)
mt.__index = function(t, k)
debug_log({'hotpatch-trace.nil-var-access', k}, nil, 3)
return nil
end
@@ -763,7 +762,7 @@ load_mod = function(installed_index)
--load/run code
debug_log({'hotpatch-info.loading'}, mod_name)
local mod_code, message = load(mod.code, '[' .. mod_name .. '] control.lua', 'bt', env)
if not mod_code then
debug_log({'hotpatch-error.compilation-failed'}, mod_name)
@@ -808,17 +807,21 @@ reset_mod = function(loaded_index)
local install_index = find_installed_mod(mod.name)
mod = global.mods[install_index]
mod.global = new_global
--local mod_global = global.mod_global[mod_name]
--for k, v in pairs(mod_global) do
-- mod_global[k] = nil
--end
reset_mod_events(loaded_index)
end
reset_mod_events = function(loaded_index)
--local loaded_index = find_loaded_mod(mod_name)
local loaded_mod = loaded_mods[loaded_index]
if not loaded_mod then
--debug_log({'hotpatch-warning.reset-events-not-running'}, mod_name)
else
local loaded_index = find_loaded_mod(mod_name)
if not loaded_index then
debug_log({'hotpatch-warning.reset-events-not-running'}, mod_name)
else
local loaded_mod = loaded_mods[loaded_index]
loaded_mod.on_event = {}
loaded_mod.on_nth_tick = {}
loaded_mod.on_init = nil
@@ -829,40 +832,17 @@ reset_mod_events = function(loaded_index)
end
end
local wrap_table
wrap_table = function(t, path)
local mt = {
__index = function(_, k)
local v = rawget(t, k)
if type(v) == 'table' then
if not v.__self then
local p = (path or 'global') .. '[' .. tostring(k) .. ']'
return wrap_table(v, p)
end
end
return v
end,
__newindex = function(_, k, v)
local k_type = type(k)
local wrap = ((k_type == 'string') and '"') or ''
local p = (path or 'global') .. '[' .. wrap .. tostring(k) .. wrap .. ']'
debug_log('WARN: Assignment to global table when none was expected: '.. p, nil, 3)
rawset(t, k, v)
end
}
return setmetatable({}, mt)
end
run_mod = function(loaded_index)
local mod = loaded_mods[loaded_index]
if mod then
local mod_name = mod.name
local old_global = mod.env.global
mod.env.global = wrap_table(old_global)
debug_log({'hotpatch-info.running'}, mod_name)
local old_global = table.deepcopy(mod.env.global)
if strict_mode then
--mod.env.global = {}
end
debug_log({'hotpatch-info.running'}, mod_name)
local success, result = xpcall(mod.code, debug.traceback)
if not success then
debug_log({'hotpatch-error.execution-failed'}, mod_name)
@@ -871,22 +851,40 @@ run_mod = function(loaded_index)
if game and game.player then
game.player.print(result)
end
return false
end
mod.env.global = old_global
old_global = nil
if strict_mode then
--if mod.env.global ~= {} then
--TODO: error, mod touched global inappropriately during load
--end
--mod.env.global = old_global
end
-- if a mod is being loaded that already has global data, it must be restored after
-- running control.lua, discarding any changes made
if not table.compare(old_global, {}) then
for k,v in pairs(mod.env.global) do
mod.env.global[k] = nil
end
for k,v in pairs(old_global) do
mod.env.global[k] = v
end
--mod.env.global = old_global
end
old_global = nil
mod.running = true
debug_log({'hotpatch-info.complete', {'hotpatch-info.running'}}, mod_name)
--load complete, start notifying on event subscriptions
if not compat_mode then
mod.env.script.on_event = function(event, f)
if event == defines.events.on_tick then
mod.on_tick = f
register_on_tick(mod_name)
register_on_tick(mod_name, event)
else
mod.on_event[event] = f
register_event(mod_name, event)
@@ -895,18 +893,18 @@ run_mod = function(loaded_index)
mod.env.script.on_nth_tick = function(tick, f)
if tick then
if type(tick) == 'table' then
for _, v in pairs(tick) do
mod.env.script.on_nth_tick(v, f)
for k, v in pairs(tick) do
mod_script.on_nth_tick(v, f)
end
return
end
mod.on_nth_tick[tick] = f
register_nth_tick(mod_name, tick)
else
local mod_on_nth_tick = mod.on_nth_tick
local on_nth_tick = mod.on_nth_tick
mod.on_nth_tick = {}
for _, v in pairs(mod_on_nth_tick) do
register_nth_tick(mod_name, v)
for k, v in pairs(on_nth_tick) do
register_nth_tick(mod_name, tick)
end
end
end
@@ -926,7 +924,7 @@ on_event = function(event)
for i = 1, #loaded_mods do
mod = loaded_mods[i]
f = mod.on_event[event.name]
if f then
if f then
debug_log({'hotpatch-trace.event-running', event_name}, mod.name)
f(event)
end
@@ -942,7 +940,7 @@ if debug_log_on_tick then
for i = 1, #loaded_mods do
mod = loaded_mods[i]
f = mod.on_nth_tick[event.nth_tick]
if f then
if f then
debug_log({'hotpatch-trace.nth-tick-event-running', tick}, mod.name)
f(event)
end
@@ -956,7 +954,7 @@ else
for i = 1, #loaded_mods do
mod = loaded_mods[i]
f = mod.on_nth_tick[event.nth_tick]
if f then
if f then
debug_log({'hotpatch-trace.nth-tick-event-running', tick}, mod.name)
f(event)
end
@@ -964,7 +962,7 @@ else
end
end
register_all_events = function()
local register_all_events = function()
--unregister all events
script.on_event(defines.events, nil)
script.on_nth_tick(nil, nil)
@@ -983,12 +981,12 @@ register_mod_events = function(loaded_index)
debug_log({'hotpatch-trace.on-tick-event-registered'}, mod_name)
script.on_event(defines.events.on_tick, on_tick)
end
for k,_ in pairs(mod.on_event) do
for k,v in pairs(mod.on_event) do
local event_name = (event_names[k] or k)
debug_log({'hotpatch-trace.on-event-registered', event_name}, mod_name)
script.on_event(k, on_event)
end
for k,_ in pairs(mod.on_nth_tick) do
for k,v in pairs(mod.on_nth_tick) do
debug_log({'hotpatch-trace.on-nth-tick-event-registered', k}, mod_name)
script.on_nth_tick(k, on_nth_tick)
end
@@ -996,14 +994,14 @@ end
mod_on_init = function(loaded_index)
local mod = loaded_mods[loaded_index]
if mod then
debug_log({'hotpatch-trace.mod-on-init'}, mod.name)
if mod.on_init then
if mod.on_init then
local success, result = xpcall(mod.on_init, debug.traceback)
if not success then
debug_log({'hotpatch-error.on-init-failed'}, mod_name)
debug_log(result, mod_name)
debug_log('on_init failed for ' .. mod.name)
debug_log(result)
return false
end
end
@@ -1015,20 +1013,17 @@ end
mod_on_load = function(loaded_index)
local mod = loaded_mods[loaded_index]
if mod then
debug_log({'hotpatch-trace.mod-on-load'}, mod.name)
if mod.on_load then
local old_global = mod.env.global
mod.env.global = wrap_table(table.deepcopy(old_global))
if mod.on_load then
--local old_global = table.deepcopy(mod.env.global)
local success, result = xpcall(mod.on_load, debug.traceback)
if not success then
debug_log({'hotpatch-error.on-load-failed'}, mod_name)
debug_log(result, mod_name)
debug_log('on_load failed for ' .. mod.name)
debug_log(result)
return false
end
mod.env.global = old_global
end
register_mod_events(loaded_index)
return true
@@ -1038,14 +1033,14 @@ end
mod_on_configuration_changed = function(loaded_index, config)
local mod = loaded_mods[loaded_index]
if mod then
debug_log({'hotpatch-trace.mod-on-configuration-changed'}, mod.name)
if mod.on_configuration_changed then
if mod.on_configuration_changed then
local success, result = xpcall(mod.on_configuration_changed, debug.traceback, config)
if not success then
debug_log({'hotpatch-error.on-configuration-changed-failed'}, mod_name)
debug_log(result, mod_name)
debug_log('on_configuration_changed failed for ' .. mod.name)
debug_log(result)
return false
end
end
@@ -1063,9 +1058,9 @@ register_on_tick = function(mod_name)
for i = 1, #loaded_mods do
mod = loaded_mods[i]
local f = mod.on_tick
if f then
found_event = true
break
if f then
found_event = true
break
end
end
debug_log({'hotpatch-trace.on-tick-event-registered'}, mod_name)
@@ -1083,9 +1078,9 @@ register_nth_tick = function(mod_name, nth_tick)
for i = 1, #loaded_mods do
mod = loaded_mods[i]
local f = mod.on_nth_tick[nth_tick]
if f then
found_event = true
break
if f then
found_event = true
break
end
end
debug_log({'hotpatch-trace.on-nth-tick-event-registered', nth_tick}, mod_name)
@@ -1131,7 +1126,7 @@ on_init = function()
debug_log({'hotpatch-info.on-init'})
--juuuuust in case
global.mods = global.mods or {}
debug_log({'hotpatch-info.installing-included-mods'})
local mod
for i = 1, #static_mods do
@@ -1142,7 +1137,7 @@ on_init = function()
for i = 1, #global.mods do
load_mod(i)
end
for i = 1, #loaded_mods do
run_mod(i)
mod_on_init(i)
@@ -1158,10 +1153,10 @@ on_load = function()
hidden_log = hidden()
debug_log({'hotpatch-info.on-load'})
debug_log({'hotpatch-info.loading-installed-mods'})
if global.globals then
error('Upgrading from Hotpatch 1.0.X to 1.1.0 is not currently supported!')
-- untested migration: use a tick to do the migration
script.on_event(defines.events.on_tick, function(e)
global.mods = {}
@@ -1179,58 +1174,52 @@ on_load = function()
global.mod_code = nil
global.mod_files = nil
global.globals = nil
on_load() -- jump back into loading
on_tick(e) -- need to test if factorio auto calls this or not after assigning the handler
script.on_event(defines.events.on_tick, on_tick) -- restore old on_tick handler
end)
return
end
-- load installed mods
for i = 1, #global.mods do
load_mod(i)
end
-- run mods which loaded successfully
local failed_mods = {}
--log('before')
--log(serpent.block(global))
-- run mods which loaded successfully
local failed_mods = {}
for i = 1, #loaded_mods do
if run_mod(i) then
if not mod_on_load(i) then
table.insert(failed_mods, i)
end
else
table.insert(failed_mods, i)
end
if not mod_on_load(i) then
table.insert(failed_mods, i)
end
else
table.insert(failed_mods, i)
end
end
-- unload mods which failed to run
for i = 1, #failed_mods do
unload_mod(failed_mods[i])
end
--log('after')
--log(serpent.block(global))
-- unload mods which failed to run
for i = 1, #failed_mods do
unload_mod(failed_mods[i])
end
debug_log({'hotpatch-info.complete', {'hotpatch-info.loading-installed-mods'}})
debug_log({'hotpatch-info.complete', {'hotpatch-info.on-load'}})
end
on_configuration_changed = function(config)
debug_log({'hotpatch-info.on-configuration-changed'})
local failed_mods = {}
local failed_mods = {}
for i = 1, #loaded_mods do
if not mod_on_configuration_changed(i, config) then
table.insert(failed_mods, i)
end
end
for i = 1, #failed_mods do
unload_mod(failed_mods[i])
if not mod_on_configuration_changed(i) then
table.insert(failed_mods, i)
end
end
for i = 1, #failed_mods do
unload_mod(failed_mods[i])
end
debug_log({'hotpatch-info.complete', {'hotpatch-info.on-configuration-changed'}})
end
@@ -1242,7 +1231,7 @@ if debug_log_on_tick then
for i = 1, #loaded_mods do
mod = loaded_mods[i]
f = mod.on_tick
if f then
if f then
debug_log({'hotpatch-trace.event-running', 'on_tick'}, mod.name)
f(e)
end
@@ -1255,7 +1244,7 @@ else
for i = 1, #loaded_mods do
mod = loaded_mods[i]
f = mod.on_tick
if f then
if f then
f(e)
end
end
@@ -1297,31 +1286,31 @@ local mod_tools_internal = setmetatable({
mod_on_init = mod_on_init,
mod_on_load = mod_on_load,
mod_on_configuration_changed = mod_on_configuration_changed,
static_mods = static_mods,
console = console,
debug_log = debug_log,
loaded_mods = loaded_mods,
loaded_libraries = loaded_libraries,
installed_mods = setmetatable({}, {
__index = function(_, k)
__index = function(t, k)
return global.mods[k]
end,
__newindex = function(_, __, ___)
__newindex = function(t, k, v)
-- do nothing
end,
__len = function(_)
__len = function(t)
return #global.mods
end,
__pairs = function(t) local function iter(_, k) local v; v = global.mods[k+1]; if v then return k+1, v end; end; return iter, t, 0 end,
__ipairs = function(t) local function iter(_, k) local v; v = global.mods[k+1]; if v then return k+1, v end; end; return iter, t, 0 end,
__pairs = function(t) local function iter(t, k) local v; v = global.mods[k+1]; if v then return k+1, v end; end; return iter, t, 0 end,
__ipairs = function(t) local function iter(t, k) local v; v = global.mods[k+1]; if v then return k+1, v end; end; return iter, t, 0 end,
__metatable = false,
})
},{
__index = function(_, k)
__index = function(t, k)
debug_log({'hotpatch-error.invalid-API-access', k}, nil, 3)
end,
__newindex = function(_, __, ___)
__newindex = function(t, k, v)
-- do nothing
end,
-- Don't let mods muck around
@@ -1333,12 +1322,11 @@ local mod_tools = setmetatable({
-- most code should use static_mod
static_mod = static_mod, -- (name, version, code, files)
set_debug_level = function(level)
--debug_level = tonumber(_ENV.debug_settings.level) or debug_levels[_ENV.debug_settings.level]
debug_level = tonumber(level) or debug_levels[level]
debug_level = tonumber(_ENV.debug_settings.level) or debug_levels[_ENV.debug_settings.level]
end,
},{
__index = mod_tools_internal,
__newindex = function(_, __, ___)
__newindex = function(t, k, v)
-- do nothing, read-only table
end,
-- Don't let mods muck around
@@ -1,5 +1,5 @@
local hotpatch_tools = require 'hotpatch.mod-tools'
hotpatch_tools.static_mod('hotpatch-remote-interface', '1.0.3', [===[
hotpatch_tools.static_mod('hotpatch-remote-interface', '1.0.2', [===[
--[[
Copyright 2018 Chrisgbk
@@ -65,7 +65,7 @@ remote_interface['install'] = function(mod_name, mod_version, mod_code, mod_file
local caller = game.player or console
if caller.admin then
if mod_files then
for k,_ in pairs(mod_files) do
for k,v in pairs(mod_files) do
if k == 'control' then
caller.print('ERROR: control.lua loaded twice for mod ' .. mod_name)
return
@@ -77,14 +77,14 @@ remote_interface['install'] = function(mod_name, mod_version, mod_code, mod_file
if installed_index then
local old_version = installed_mods[installed_index].version
if old_version then
debug_log({'hotpatch-warning.already-exists', mod_name, old_version})
debug_log({'hotpatch-warning.reinstalling', mod_name, old_version})
debug_log('WARNING: mod already exists: ' .. mod_name .. ' ' .. old_version)
debug_log('WARNING: reinstalling mod in-place: ' .. mod_name .. ' ' .. mod_version)
end
if loaded_index then
unload_mod(loaded_index)
end
end
install_mod(mod_name, mod_version, mod_code, mod_files)
if not only_install then
installed_index = find_installed_mod(mod_name)
@@ -131,8 +131,6 @@ remote_interface['run'] = function(mod_name)
if caller.admin then
local installed_index = find_installed_mod(mod_name)
local loaded_index = find_loaded_mod(mod_name)
local mod
local version
if installed_index and not loaded_index then
if not load_mod(installed_index) then
caller.print('compilation failed for mod ' .. mod_name)
@@ -141,13 +139,13 @@ remote_interface['run'] = function(mod_name)
loaded_index = find_loaded_mod(mod_name)
else
if loaded_index then
mod = loaded_mods[loaded_index]
version = mod.version
debug_log({'hotpatch-warning.already-loaded', mod_name, version})
debug_log({'hotpatch-warning.reinitializing', mod_name, version})
local mod = loaded_mods[loaded_index]
local version = mod.version
debug_log('WARNING: mod already loaded: ' .. mod_name .. ' ' .. version)
debug_log('WARNING: reinitializing: ' .. mod_name .. ' ' .. version)
end
end
if loaded_index then
if not run_mod(loaded_index) then
caller.print('execution failed for mod ' .. mod_name)
@@ -166,7 +164,7 @@ remote_interface['run'] = function(mod_name)
unload_mod(loaded_index)
return
end
end
end
else
caller.print('You must be an admin to run this command.')
end
@@ -180,7 +178,7 @@ remote_interface['update'] = function(mod_name, mod_version, mod_code, mod_files
local caller = game.player or console
if caller.admin then
if mod_files then
for k,_ in pairs(mod_files) do
for k,v in pairs(mod_files) do
if k == 'control' then
caller.print('ERROR: control.lua loaded twice for mod ' .. mod_name)
return
@@ -188,9 +186,6 @@ remote_interface['update'] = function(mod_name, mod_version, mod_code, mod_files
end
end
local installed_index = find_installed_mod(mod_name)
if installed_index then
;
end
local loaded_index = find_loaded_mod(mod_name)
local old_version
if loaded_index then
@@ -250,40 +245,16 @@ remote_interface['install_mod_file'] = function(mod_name, mod_file, mod_file_cod
end
end
remote_interface['exec'] = function(mod_name, exec_code)
local caller = game.player or console
if caller.admin then
local loaded_index = find_loaded_mod(mod_name)
local env = loaded_mods[loaded_index].env
local success
local code, err = load(exec_code, exec_code, 't', env)
if code then
success, err = pcall(code)
if not success then
debug_log(err)
end
else
debug_log(err)
end
else
caller.print('You must be an admin to run this command.')
end
end
--TODO: most of this function
remote_interface['clean'] = function(mod_name)
-- Removes ALL mod data and reinitializes
-- doesn't remove things a mod may add like surfaces, etc
local caller = game.player or console
if caller.admin then
--mod_reset(mod_name)
--run_mod(mod_name)
--mod_on_init(mod_name)
local loaded_index = find_loaded_mod(mod_name)
local mod = loaded_mods[loaded_index]
local version = mod.version
mod_on_configuration_changed(loaded_index, {mod_changes = {mod_name={new_version=version}}})
mod_reset(mod_name)
run_mod(mod_name)
mod_on_init(mod_name)
mod_on_configuration_changed(mod_name, {mod_changes = {mod_name={new_version=mod_version}}})
else
caller.print('You must be an admin to run this command.')
end
@@ -303,13 +274,13 @@ remote.add_interface('hotpatch', remote_interface)
debug_log({'hotpatch-info.complete', {'hotpatch-info.remote-installing'}})
--]===])
]===])
--[[
remote.call('hotpatch, 'install', 'test', '1.0.0', [===[
script.on_event(defines.events.on_player_changed_position, function(e)
game.print('changed position')
remote.call('hotpatch, 'install', 'test', '1.0.0', [===[
script.on_event(defines.events.on_player_changed_position, function(e)
game.print('changed position')
end
]===]
@@ -1,3 +1,3 @@
msg-intro=Hierdie is Factorio vryspel. Jou taak is om 'n vuurpyl in die ruimte in te stuur. Doen dit deur 'n vuurpyl te bou en hom te lanseer. Jy sal baie gevorderde tegnologiëe moet navors om die Vuurpyl Silo te ontsluit. Begin klein, werk jou pad op met outomatisering en moenie vergeet om jouself te beskerm teen die inboorlinge nie.
scenario-name=Vryspel
msg-intro=Hierdie is Factorio vryspel. Jou taak is om 'n vuurpyl in die buitenste ruimte in te stuur. Doen dit deur die bou van 'n Vuurpyl Silo en van daar af 'n vuurpyl te lanseer met a satelliet aan boord. Jy sal baie gevorderde tegnologiëe moet navors om die Vuurpyl Silo te ontsluit. Begin klein, werk jou pad op met outomatisering en moenie vergeet om jouself te beskerm teen die inboorlinge nie.
@@ -1,3 +1,3 @@
msg-intro=.بكوكلا اذه ناكس م كسفن يمحت نا ىسنت الو ,ةيكيتاموتوألا ربع كقيرط ينبتسو ,رفصلا نم أدبتس .كلت خيراوصلا قالطإ ةطحم حتف عيطتست فيكل ةروطتملا ايجولونكتلل ثاحبأب مايقلا ىلإ جاتحتس .اهنم خوراص قالطإ مث خيراوص قالطإ ةطحم ءانب قيرط نع كلذب مايقلا كنكمي ,ءاضفلا ىلإ اخوراص قلطت نا يه كتمهم ,factorioـل رحلا بعللا روط وه اذه
scenario-name=رحلا بعللا
msg-intro=.نييلصألا ناكسلا نم كسفن ةيامح ىسنت الو ةياهنلا ىلإ لصتل كتقيرطب لمعأو ،رفصلا نم أدبتس .خيراوصلا عماوص حتف لجأ نم ةمدقتم ايجولونكت نع ثحبلل جاتحت فوس .ةيعانصلا رامقألاب خيراوصلا قالطإو خيراوصلا عماوص ءانب قيرط نع كلذب مايقلا عيطتست .ءاضفلل اخوراص قلطت نا يه كتمهم .Factorio ـل رحلا بعللا وه اذه
@@ -1,3 +1,3 @@
msg-intro=Гэта рэжым свабоднай гульні Factorio. Вашая мэта - запусціць ракету ў космас. Дзеля гэтага вам трэба пабудаваць ракетную шахту і саму ракету са спадарожнікам. Вы мусіце даследаваць перадавыя тэхналогіі, каб навучыцца будаваць ракетную шахту. Пачынайце з малога, працуйце над аўтаматызацыяй вытворчасці, даследуйце новыя тэхналогіі і не забывайцеся на абарону ад «тутэйшых».
scenario-name=Свабодная гульня
msg-intro=Гэта рэжым свабоднай гульні Factorio. Вашая мэта - запусціць ракету ў космас. Дзеля гэтага вам трэба пабудаваць ракетную шахту і саму ракету з спадарожнікам. Вы мусіце даследваць перадавыя тэхналогіі, каб навучыцца будаваць ракетную шахту. Пачынайце з малога, працуйце над аўтаматызацыяй вытворчасці, даследуйце новыя тэхналогіі і не забывайцеся на абарону ад "тутэйшых"!
@@ -1,3 +1,3 @@
msg-intro=Това е режимът на свободна игра. Задачата ви е да изстреляте ракета в космоса. За това ще е нужно да построите ракетна шахта и изстреляте ракетата със спътник. Ще трябва да проучите развити технологии, които ще отключат ракетната шахта. Започнете с малко и постепенно автоматизирайте процеса, като не забравяте да се защитавате от местните форми на живот.
scenario-name=Свободна игра
@@ -1,3 +1,3 @@
msg-intro=Aquest és el mode lliure del Factorio. La vostra tasca és construir un coet i enviar-lo a l'espai. Ho haureu de fer construint un centre de coets espacials i llençant un coet. Necessitareu investigar tecnologies avançades per poder-ho fer. Aneu pas a pas, avanceu cap a l'automatització i no oblideu protegir-vos dels natius.
scenario-name=Mode lliure
msg-intro=Aquest és el mode lliure del Factorio. La vostra tasca és construir un coet i enviar-lo a l'espai. Ho haureu de fer construint un centre de coets espacials i llençant un coet amb un satèl·lit. Necessitareu investigar tecnologies avançades per poder-ho fer. Aneu pas a pas, avanceu cap a l'automatització i no oblideu protegir-vos dels natius.
@@ -1,3 +1,3 @@
msg-intro=Toto je volná hra Factorio. Tvým úkolem je vypustit raketu do vesmíru. Postav raketové silo a vypusť raketu. Pro odemknutí raketového sila budeš muset vyzkoumat pokročilé technologie. Začni v malém, postupuj s automatizací a nezapomeň se chránit před domorodci.
scenario-name=Volná hra
msg-intro=Toto je volná hra Factorio. Tvým úkolem je vypustit raketu do vesmíru. Postav raketové silo a vypusť raketu se satelitem. Pro odemknutí raketového sila budeš muset vyzkoumat pokročilé technologie. Začni v malém, postupuj s automatizací a nezapomeň se chránit před domorodci.
@@ -1,3 +1,3 @@
msg-intro=Dette er Factorio freeplay. Din opgave er at affyre en raket ud i rummet. Gør dette ved at konstruere en raketsilo og affyr en raket. Du bliver nød til at undersøge avancerede teknologier for at låse op for raketsiloen. Start småt og arbejd din vej op med automatisering og glem ikke at beskytte dig selv fra de indfødte.
scenario-name=Fri lej
msg-intro=Det her er Factorio fri leg. Din opgave er at affyre en raket for at udforske rummet. For at gøre det skal du bygge en raket silo og affyre en raket med en satellit i. Du bliver nød til at udforske avancerede teknologier for at at kunne bygge en raket silo. Start småt, og arbejd din vej op med hjælp af automatisering, glem ikke at beskytte dig fra de indfødte.
@@ -1,3 +1,3 @@
msg-intro=Du befindest dich im freien Spiel Modus von Factorio. Ziel ist es, eine Rakete ins Weltall zu schicken. Baue hierzu das Raketensilo um die Rakete starten zu können. Um das Raketensilo freischalten zu können wirst du fortgeschrittene Technologien erforschen müssen. Fange klein an und erforsche die Automation. Vergiss aber nicht, dich vor den Ureinwohner zu schützen.
scenario-name=Freies Spiel
msg-intro=Dies ist das freie Spiel von Factorio. Deine Aufgabe ist es, eine Rakete in den Weltraum zu starten. Dazu musst Du ein Raketensilo bauen und eine Rakete mit einem Satelliten starten. Du wirst erweiterte Technologien erforschen müssen, um das Raketensilo freizuschalten. Fange klein an, arbeite Dich durch Automatisierung hoch und vergiss nicht, Dich vor den Einheimischen zu schützen.
@@ -1,3 +1,3 @@
msg-intro=Αυτό είναι το ελεύθερο παιχνίδι του Factorio. Ο στόχος σας είναι να εκτοξεύσετε έναν πύραυλο στο διάστημα. Αυτό το κάνετε κατασκευάζοντας ένα σιλό πυραύλου και εκτοξεύοντας έναν πύραυλο. Θα πρέπει να ερευνήσετε προηγμένες τεχνολογίες για να ξεκλειδώσετε το σιλό πυραύλου. Ξεκινήστε χαμηλά, φτάστε ψηλά χρησιμοποιώντας αυτοματισμούς και μην ξεχάσετε να προστατέψετε τον εαυτό σας από τους ντόπιους.
scenario-name=Ελεύθερο παιχνίδι
msg-intro=Αυτό είναι το ελεύθερο παιχνίδι του Factorio. Ο στόχος σου είναι να εκτοξεύσεις έναν πύραυλο στο διάστημα. Αυτό το κάνεις κατασκευάζοντας ένα σιλό πυραύλου και εκτοξεύοντας έναν πυραύλο με δορυφόρο. Θα πρέπει να ερευνήσεις προηγμένες τεχνολογίες για να ξεκλειδώσεις το σιλό πυραύλου. Ξεκινάς περιορισμένος, δούλεψε με αυτοματισμούς για την άνοδο σου και μην ξεχνάς να προστατέψεις τον εαυτό σου από τους ντόπιους.
@@ -1,2 +1,2 @@
msg-intro=This is the Factorio freeplay. Your task is to launch a rocket into space. Do this by constructing a rocket silo and launching a rocket. You will need to research advanced technologies in order to unlock the rocket silo. Start small, work your way up with automation and don't forget to protect yourself from the natives.
scenario-name=Freeplay
msg-intro=This is the Factorio freeplay. Your task is to launch a rocket into space. Do this by constructing a Rocket Silo and launching a rocket with a satellite. You will need to research advanced technologies in order to unlock the Rocket Silo. Start small, work your way up with automation and don't forget to protect yourself from the natives.
@@ -65,17 +65,13 @@ contains-comments-console=comments from console will comment out the entire code
reset-events-not-running=tried to reset events for mod that isn't running!
remote-interface-exists=remote interface __1__ already exists, removing...
command-exists=command __1__ already exists, removing...
already-exists=mod already exists: __1__ __2__
reinstalling=reinstalling mod in-place: __1__ __2__
already-loaded=mod already loaded: __1__ __2__
reinitializing=reinitializing: __1__ __2__
already-loaded=mod already loaded
reinitializing=reinitializing...
already-exists=mod already exists
reinstalling=reinstalling mod in-place
[hotpatch-error]
invalid-API-access=Invalid API access: __1__
not-installed=mod not installed, cannot install file for mod that does not exist
compilation-failed=compilation failed for mod
execution-failed=execution failed for mod
path-not-found=path '__1__' not found
on-init-failed=on_init failed
on-load-failed=on_load failed
on-configuration-changed-failed=on_configuration_changed failed
@@ -1,2 +1,3 @@
scenario-name=Libera Ludado
msg-intro=Tio estas la Factorio Libera Ludado. Via tasko estas ĵeti raketon en spacon. Fari tiun per konstruanta Raketo Silo kaj ĵeti raketon kun satelito. Vi bezonos esplori altnivelaj teknologioj por malŝlosi la Raketo Silo. Start malgranda, labori vian vojon supren per aŭtomatigo kaj ne forgesu protekti vin mem de la indiĝenoj.
@@ -1,3 +1,3 @@
msg-intro=Este es el modo de juego libre de Factorio. Tu tarea es la de lanzar un cohete al espacio. Logra esto construyendo un Silo de Cohetes y lanzando un cohete con un satélite. Necesitarás investigar tecnologías avanzadas para poder desbloquear el Silo de Cohetes. Comienza poco a poco, trabaja en tu sistema de automatización y no te olvides de protegerte de los nativos.
scenario-name=Modo libre
@@ -1,2 +1,3 @@
scenario-name=Vabamäng
msg-intro=See on Factorio vabamäng. Sinu eesmärgiks on saata rakett avakosmosesse. Seda saad teha ehitades Raketi Silotorni ja lennutades Raketti koos satelliidiga. Selleks pead õppima selgeks keerukad tehnoloogiaharud, et Raketi Silotorni lahti lukustada. Alusta väikselt ning liigu edasi automatiseerimise abil ja ära unusta ennast kaitsta kohalike eluvormide eest.
@@ -1,3 +1,3 @@
msg-intro=Tämä on Factorion vapaapelimuoto. Tavoitteesi on lähettää satelliitti ulkoavaruuteen rakentamalla rakettisiilo ja laukaisemalla raketti. Rakettisiilon voit rakentaa vasta, kun olet kehittänyt siihen tarvittavat huipputeknologiat. Aloita pienestä ja automatisoi kaikki tarvittava, äläkä unohda suojella itseäsi alkuasukkailta.
scenario-name=Vapaapeli
@@ -1,3 +1,3 @@
msg-intro=Il s'agit du mode de jeu libre de Factorio. Votre tâche consiste à lancer une fusée dans l'espace. Faites ceci en construisant une fusée et en la lançant dans l'espace. Vous aurez besoin de rechercher des technologies de pointe afin de débloquer le silo. Commencez petit, évoluez en automatisant et n'oubliez pas de vous protéger contre les autochtones.
scenario-name=Jeu libre
msg-intro=Il s'agit du mode de jeu libre de Factorio. Votre tâche consiste à lancer une fusée dans l'espace. Ceci en construisant un silo à fusée, et en lançant un satellite avec la fusée. Vous aurez besoin de rechercher des technologies de pointe afin de débloquer le silo. Commencez petit, évoluez en automatisant et n'oubliez pas de vous protéger contre les autochtones.
@@ -1,3 +1,3 @@
msg-intro=.םיימוקמה םירוציה ינפמ םכמצע לע ןגהל וחכשת לאו ,יטמוטוא ןוכימ תועצמאב םכלש הישעתה תא וביחרה ,ןטקב וליחתה .ןקתמה תיינב תא רשפאל ידכ תומדקתמ תויגולונכט רוקחל םכילע היהי .ליט ותועצמאב רגשלו םיליט רוגיש ןקתמ תונבל םכילע .ללחל ליט רגשל איה םכתמישמ .ויִרֹוטְקַפ לש ישפוחה שיחרתה והז
scenario-name=ישפוח קחשמ
msg-intro=.םיימוקמה םירוציה ינפמ םכמצע לע ןגהל וחכשת לאו ,יטמוטוא ןוכימ תועצמאב םכלש הישעתה תא וביחרה ,ןטקב וליחתה .ןקתמה תיינב תא רשפאל ידכ תומדקתמ תויגולונכט רוקחל םכילע היהי .ןייוול ליכמה ליט ותועצמאב רגשלו םיליט רוגיש ןקתמ תונבל םכילע .ללחל ליט רגשל איה םכתמישמ .ויִרֹוטְקַפ לש ישפוחה שיחרתה והז
@@ -1,3 +1,3 @@
msg-intro=Ez a Factorio szabad játék módja. A feladatod az, hogy kilőj egy rakétát az űr felderítésére. Ehhez egy Rakétasilót kell építened, melyből aztán egy műholddal felszerelt rakétát kell kilőnöd. A Rakétasiló kifejlesztéséhez különféle technológiákat kell fejlesztened. Kezdj kicsiben és küzdd fel magad az automatizálásban. Ne felejts el védekezni a honos fajok ellen.
scenario-name=Szabad játék
@@ -1,3 +1,3 @@
msg-intro=Questa è la modalità libera di Factorio. Il tuo obiettivo è quello di lanciare un razzo nello spazio e per farlo dovrai costruire un silo per razzi. Avrai bisogno di fare ricerche avanzate per sbloccare il silo. Inizia con poco, lavora sull'automazione e non dimenticarti di difenderti dai nativi.
scenario-name=Gioco libero
msg-intro=Questa è la modalità libera di Factorio. Il tuo obiettivo è quello di lanciare il razzo nello spazio. Completalo costruendo un Silo per Razzi e lanciando il razzo con il satellite. Avrai bisogno di ricercare tecnologie avanzate per riuscire a sbloccare il Silo per Razzi. Inizia in piccolo, espandi con l'automazione e non dimenticare di proteggerti dai nativi.
@@ -1,3 +1,3 @@
msg-intro=これは、Factorioのフリープレイです。ロケットサイロを建造し、ロケットを宇宙へ打ち上げるのがあなたの任務です。ロケットサイロの建造を行うためには高度な技術研究を行う必要があります。小規模に始め、自動化しつつ進み、原住生物から身を守ることも忘れないでください。
scenario-name=フリープレイ
msg-intro=これは、Factorioのフリープレイです。ロケットサイロを建造し、衛星を載せたロケットを宇宙へ打ち上げるのがあなたの任務です。ロケットサイロをアンロックするためには高度な技術研究をする必要があります。小規模に始め、自動化しつつ進み、原住生物から身を守ることも忘れないでください。
@@ -1,3 +1,3 @@
msg-intro=Factorio 자유플레이입니다. 당신의 목표는 우주로 로켓을 발사하는 것입니다. 로켓 격납고를 건설하고 로켓을 발사하여 이 목표를 달성하세요. 로켓 격납고의 잠금을 해제하기 위해 고급 기술들을 연구할 필요가 있습니다. 작은 것부터 시작해 자동화로 차근차근 발전하고, 토착 생물로부터 자신을 보호하는 것도 잊지 마세요.
scenario-name=자유플레이
msg-intro=Factorio 자유플레이입니다. 당신의 목표는 우주로 로켓을 발사하는 것입니다. 로켓 격납고를 건설하고 인공위성을 탑재한 로켓을 발사하여 이 목표를 달성하세요. 로켓 격납고의 잠금을 해제하기 위해 고급 기술들을 연구할 필요가 있습니다. 작은 것부터 시작해 자동화로 차근차근 발전하고, 토착 생물로부터 자신을 보호하는 것도 잊지 마세요.
@@ -1,3 +1,3 @@
msg-intro=Tai Factorio laisvas žaidimas. Jūsų užduotis - paleisti raketą į kosmosą. Tam reikia pastatyti Raketos šachtą ir paleisti raketą su palydovu. Reikės atrasti pažangias technologijas kad atrakintumėte Raketos šachtą. Pradėkite palengva, tobulinkite automatiką ir neužmirškite apsisaugoti nuo vietinių.
scenario-name=Žaisti laisva forma
@@ -1,2 +1,2 @@
scenario-name=Brīvspēle
msg-intro=Šis ir Factorio brīvais režīms. Tavs uzdevums ir palaist raķeti kosmosā. Lai to izdarītu, tev vajadzēs uzbūvēt palaišanas platformu un palaist raķeti ar satelītu. Tev būs jāizpēta sarežģītas tehnoloģijas, lai atbloķētu palaišanas platformu. Sāc no mazumiņa, strādā un neaizmirsti pasargāt sevi no šīs planētas iemītniekiem...
@@ -1,3 +1,3 @@
msg-intro=Dit is Factorio vrij spel. Je taak is om een raket te lanceren in de ruimte. Bouw hiervoor een raket-silo en lanceer een raket. Je zult geavanceerde technologieën moeten onderzoeken om de raket-silo vrij te spelen. Begin op kleine schaal, breidt uit en automatiseer je fabriek en vergeet niet jezelf te beschermen tegen de inheemse bewoners.
scenario-name=Freeplay
msg-intro=Dit is Factorio vrij spel. Je taak is om een raket te lanceren in de ruimte. Bouw hiervoor een raket-silo en lanceer een raket met een satelliet. Je zult geavanceerde technologieën moeten onderzoeken om de raket-silo vrij te spelen. Begin op kleine schaal, breidt uit en automatiseer je fabriek en vergeet niet jezelf te beschermen tegen de inheemse bewoners.
@@ -1,3 +1,3 @@
msg-intro=Dette er Factorio freeplay. Din oppgave er å sende en rakett til verdensrommet. Bygg en rakettsilo og send opp en rakett med en satellitt. Du blir nødt til å utforske avanserte teknologier for å få tilgang til rakettsiloen. Begynn i det små, jobb deg oppover med hjelp av automatisering, men ikke glem å beskytte deg mot de innfødte.
scenario-name=Frittspill
msg-intro=Dette er Factorio frispilling. Din oppgave er å sende en rakett til verdensrommet. Bygg en rakettsilo og send opp en rakett med en satellitt. Du blir nødt til å utforske avanserte teknologier for å få tilgang til rakettsiloen. Begynn i det små, jobb deg oppover med hjelp av automatisering, men ikke glem å beskytte deg mot de innfødte.
@@ -1,3 +1,3 @@
msg-intro=Oto swobodna rozgrywka Factorio. Twoim zadaniem jest wysłanie rakiety w kosmos. Dokonaj tego przez skonstruowanie silosu rakietowego i wystrzelenie rakiety. Żeby odblokować silos rakietowy będziesz musiał odkryć zaawansowane technologie. Rozpocznij od podstaw, rozwijaj się wykorzystując automatyzację i nie zapomnij o obronie przed rodzimymi mieszkańcami planety.
scenario-name=Gra swobodna
msg-intro=To jest swobodna rozgrywka Factorio. Twoim zadaniem jest wysłanie rakiety w kosmos. Skonstruuj silos rakietowy i wystrzel z niego rakietę z satelitą. Żeby odblokować silos rakietowy będziesz musiał odkryć zaawansowane technologie. Rozpocznij od podstaw, rozwijaj się wykorzystując automatyzację i nie zapomnij o obronie przed rodzimymi mieszkańcami planety.
@@ -1,3 +1,3 @@
msg-intro=Este é o modo livre de Factorio. Sua tarefa é lançar um foguete no espaço. Faça isso construindo um silo de foguetes e lançando um foguete. Você precisará pesquisar tecnologias avançadas para desbloquear o silo de foguetes. Comece pequeno, trabalhe seu caminho com automação e não se esqueça de se proteger dos nativos.
scenario-name=Jogo livre
msg-intro=Este é o modo livre do Factorio. Sua missão é lançar um foguete no espaço. Para fazer isso construa um centro de lançamento de foguetes e lance o foguete com um satélite. Você vai precisar pesquisar tecnologias avançadas para conseguir desbloquear um Centro de lançamento. Comece do começo, continue na automação e não se esqueça de se proteger dos nativos.
@@ -1,3 +1,3 @@
msg-intro=Este é o modo livre do Factorio. O teu objetivo é lançar um foguetão para o espaço. Faz isto, contruindo um silo de foguetão e lançando-o.\nVais precisar de pesquisar tecnologias avançadas para desploquear o silo de foguetão. Começa simples, e trabalha e evolui com automação mas não te esqueças de proteger do nativos.
scenario-name=Modo Livre
msg-intro=Este é o jogo livre Factorio. A tua tarefa é lançar um foguetão para explorar o espaço. Faça isto, construindo um Silo de Foguetão e lançar um foguetão com um satélite. Tu vais precisar de pesquisar tecnologias avançadas para poderes desbloquear o Silo de Foguetão. Começa simples, trabalha e evolui com a automação e não te esqueças de proteger-te dos nativos.
@@ -1,3 +1,3 @@
msg-intro=Acesta este modul Factorio freeplay. Sarcina ta este de a lansa o racheta în spațiu. Fă acest lucru prin construirea unui siloz de rachete și lansarea unei rachete. Va trebui să cercetezi tehnologiile avansate pentru a debloca silozul de rachete. Începe mic, avansează cu automatizarea și nu uita să te protejezi de nativi.
scenario-name=Freeplay
msg-intro=Acesta este modul Factorio freeplay. Sarcina ta este de a lansa o rachetă în spațiu. Fă acest lucru prin construirea unui siloz de rachete și lansarea unei rachete cu satelit. Va trebui să cercetezi tehnologiile avansate pentru a debloca silozul de rachete. Începe încet, avansează cu automatizarea și nu uita să te protejezi de inamici.
@@ -1,3 +1,3 @@
msg-intro=Это режим свободной игры Factorio. Ваша задача - запустить ракету в космос. Для её запуска потребуется исследовать новые технологии и создать ракетную шахту. Начните с малого, работайте над автоматизацией производства и не забывайте о защите от местных форм жизни.
scenario-name=Свободная игра
msg-intro=Это режим свободной игры Factorio. Ваша задача запустить спутник в космос. Для его запуска потребуется создать ракету и ракетную шахту. Начните с малого, работайте над автоматизацией производства, исследуйте новые технологии и не забывайте о защите от местных форм жизни.
@@ -1,3 +1,3 @@
msg-intro=Toto je voľná hra Factorio. Tvojou úlohou je vypustiť raketu do vesmíru. Postav raketové silo a vypusti raketu. Pre odomknutie raketového sila budeš musieť vyskúmať pokročilé technológie. Začni v malom, postupuj s automatizáciou a nezabnudni sa chrániť pred domácimi.
scenario-name=Voľná hra
msg-intro=Toto je voľná hra Factorio. Tvojou úlohou je vypustiť raketu do vesmíru. Postav raketové silo a vypusti raketu so satelitom. Pre odomknutie raketového sila budeš musieť vyskúmať pokročilé technológie. Začni v malom, postupuj s automatizáciou a nezabnudni sa chrániť pred domácimi.
@@ -1,3 +1,3 @@
msg-intro=To je Factorio freeplay. Vaša naloga je poslati raketo v vesolje. To storite tako, da zgradite silos za raketo in jo skupaj s satelitom izstrelite v vesolje. Raziskati bo potrebno napredne tehnologije, da bi lahko odklenili Raketni silos. Začnete z majhnim, nato nadgradite vse s pomočjo avtomatizacije in ne pozabite, zaščitite se pred domorodci.
scenario-name=Prosta igra
@@ -1,3 +1,3 @@
msg-intro=Det här är Factorio frispel. Ditt uppdrag är att avfyra en raket ut i rymden. Gör det genom att tillverka en raket silo och avfyra en raket med en satellit. Du kommer att behöva forska fram avancerade teknologier för att kunna låsa upp raket silon. Börja smått, jobba dig uppåt med hjälp av automatisering och glöm inte att försvara dig själv mot infödingarna.
scenario-name=Frispel
@@ -1,2 +1,3 @@
scenario-name=โหมดอิสระ
msg-intro=นี้คือโหมดอิสระของ Factorio ภารกิจของคุณคือส่งจรวดสู่อวกาศ ซึ่งทำได้โดยการสร้างไซโลจรวดและส่งจรวดขึ้นไปพร้อมกับดาวเทียม คุณต้องวิจัยค้นคว้าเทคโนโลยีขั้นสูงเพื่อปลดล็อคไซโลจรวด เริ่มจากเล็กๆไปเรื่อยๆด้วยระบบอัตโนมัติและอย่าลืมป้องกันตัวเองจากคนพื้นเมืองด้วย
@@ -1,3 +1,3 @@
msg-intro=Bu Factorio'nun serbest oynanışıdır. Göreviniz uzaya bir roket göndermek. Bunu roket rampası inşa edip ve Roket fırlatarak yapınız. Roket rampasını açmak için gelişmiş teknolojileri araştırmanız gerekecek. Ufaktan başlayın ve otomasyonla büyüyün, ve yerlilerden kendiniz korumayı unutmayın.
scenario-name=Serbest Oyun
msg-intro=Bu Factorio'nun serbest oynanışı. Görevin roketleri uzaya yollamak. Bunu Roket Silosu yaparak ve uyduyla roket yollayarak yapacaksın. Roket Silo'sunu açmak için gelişmiş teknolojileri sırayla açmalısın. Yavaş başla, otomatikleş ve yerlilerden kendini korumayı untuma.
@@ -1,3 +1,3 @@
msg-intro=Це один з типів гри в Factorio — "Вільна гра". Ваше завдання — запустити ракету у космічний простір. Зробіть це, побудувавши ракетну шахту та запустивши ракету. Вам потрібно дослідити передові технології, щоб мати змогу побудувати ракетну шахту. Починайте з малого, розвивайтеся за допомогою автоматизації, але не забувайте і про оборону.
scenario-name=Вільна гра
msg-intro=Це один з типів гри в Factorio - "Вільна гра". Ваше завдання - запустити ракету у космічний простір. Зробіть це, побудувавши ракетну шахту та запустивши ракету зі супутником. Вам потрібно дослідити передові технології, щоб мати змогу побудувати ракетну шахту. Починайте з малого, розвивайтеся за допомогою автоматизації, але не забувайте і про оборону!
@@ -1,3 +1,3 @@
msg-intro=Đầy là chế độ chơi tự do của Factorio. Nhiệm vụ của bạn là phóng một chiếc tên lửa lên vũ trụ bằng cách xây dựng một bệ phóng. Bạn sẽ cần phải nghiên cứu nhiều công nghệ tiên tiến để có thể xây dựng bệ phóng đó. Hãy bắt đầu nhỏ, rồi phát triển từng bước bằng tự động hóa, và đừng quên bảo vệ chính mình khỏi những sinh vật ở đây.
scenario-name=Chơi tự do
msg-intro=Đầy là chế độ freeplay của Factorio. Nhiệm cụ của bạn là phóng một chiếc tên lửa lên vũ trụ. Hãy xây dựng một bệ phóng tên lửa để phóng một chiếc tên lửa kèm theo vệ tinh. Bạn sẽ cần phải nghiên cứu nhiều công nghệ tiên tiến để có thể xây dựng bệ phóng tên lửa. Bắt đầu từ quy mô nhỏ, phát triển dần dần bằng tự động hóa, và đừng quên bảo vệ bản thân mình khỏi những sinh vật sinh sống ở đây.
@@ -1,3 +1,3 @@
msg-intro=欢迎来到异星工厂标准模式,你的目标是要把火箭发射到太空。为此你需要研发一系列高等科技,修建火箭发射井,最后发射火箭。从小作坊生产起步,逐渐建立起属于自己的自动化大工厂吧!同时,别忘了防范异星虫子们的骚扰 ...
scenario-name=标准模式
msg-intro=欢迎来到异星工厂标准模式,你的目标是要把火箭发射到太空。为此你需要研发一系列高等科技,解锁并修建火箭发射井,直到把携带有卫星的火箭发射出去。从小作坊生产起步,逐渐建立起属于自己的自动化大工厂吧!同时,别忘了防范异星虫子的骚扰 ...
@@ -1,3 +1,3 @@
msg-intro=這是異星工廠的自由模式。你的任務是發射火箭探索外太空。請建造火箭發射井並發射火箭。你會需要研究大量的進階科技以解鎖火箭發射井。從頭開始,靠著自動化的協助邁向目標的同時,也別忘了從外星住民手中保護你自己。
scenario-name=自由模式
msg-intro=這是異星工廠的自由模式。你的任務是發射火箭,探索外太空。請建造一個火箭發射井,並發射攜帶著衛星的火箭。你會需要研究大量的進階科技以解鎖火箭發射井。從頭開始,靠著自動化科技的協助向目標邁進時,也別忘記從外星住民手的中保護你自己啊!
Vendored
+16
View File
@@ -0,0 +1,16 @@
type MasterPluginInstance = {
config: object,
pluginConfig: object,
path: string,
socketio: any,
express: any,
Prometheus: any,
onExit?: Function,
}
type pluginConfig = {
name: string,
}
interface MasterPlugin {
main: MasterPluginInstance,
pluginConfig: pluginConfig,
}
-11
View File
@@ -76,17 +76,6 @@ describe('Master server endpoint testing', function() {
});
});
});
it("sends some HTML when accessing /remoteMap",function(done){
this.timeout(6000);
request(app).get("/remoteMap").end(function(err,res){
assert.equal(res.statusCode, 200);
validateHTML(res.text).then(result => {
// there should be 1 error regarding complaining about me using ES6 modules before they are fully supported
assert(result.messages.length === 2, "there are HTML errors on the page, please fix: "+JSON.stringify(result.messages));
done();
});
});
});
});
let persistentMaster = request(app);
describe("#POST /api/place", function(){
+239 -287
View File
@@ -1,3 +1,6 @@
import { getDatabase } from "./lib/getDatabase";
import { db } from "./lib/getDatabase";
/**
Clusterio master server. Facilitates communication between slaves through
a webserver, storing data related to slaves like production graphs and
@@ -13,7 +16,9 @@ node master.js
// Attempt updating
// const updater = require("./updater.js");
// updater.update().then(console.log);
namespace Master {
// homebrew types
//import {MasterPlugin} from "master"
(async ()=>{
// Set the process title, shows up as the title of the CMD window on windows
@@ -24,7 +29,11 @@ process.title = "clusterioMaster";
process.on('unhandledRejection', r => console.log(r));
// configgy stuff
debug = false;
const debug = false;
// reduces loadtime from 1700 ms to 1200 ms on my i3 + SSD
// tested by logging Date.now() here and when the webserver started listening
require('cache-require-paths');
// argument parsing
const args = require('minimist')(process.argv.slice(2));
@@ -36,13 +45,14 @@ const deepmerge = require("deepmerge");
const path = require("path");
const fs = require("fs-extra");
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const crypto = require('crypto');
const base64url = require('base64url');
const moment = require("moment");
const request = require("request");
const request = require("request")
// constants
console.log(`Requiring config from ${args.config || './config'}`);
console.log(`Requiring config from ${args.config || './config'}`)
const config = require(args.config || './config');
config.databaseDirectory = args.databaseDirectory || config.databaseDirectory || "./database";
const masterModFolder = path.join(config.databaseDirectory, "/masterMods/");
@@ -50,18 +60,20 @@ mkdirp.sync(config.databaseDirectory);
mkdirp.sync(masterModFolder);
// homebrew modules
const getFactorioLocale = require("lib/getFactorioLocale");
const getFactorioLocale = require("./lib/getFactorioLocale");
const stringUtils = require("./lib/stringUtils");
const fileOps = require("_app/fileOps");
// homemade express middleware for token auth
const authenticate = require("lib/authenticate")(config);
const authenticate = require("./lib/authenticate")(config);
/** Sync */
function randomStringAsBase64Url(size) {
function randomStringAsBase64Url(size: Number) {
return base64url(crypto.randomBytes(size));
}
if (!fs.existsSync("secret-api-token.txt")) {
config.masterAuthSecret = randomStringAsBase64Url(256);
fs.writeFileSync("config.json",JSON.stringify(config, null, 4));
fs.writeFileSync("config.json",JSON.stringify(config, null, 4));
fs.writeFileSync("secret-api-token.txt", jwt.sign({ id: "api" }, config.masterAuthSecret, {
expiresIn: 86400*365 // expires in 1 year
}));
@@ -98,11 +110,11 @@ app.set('view engine', 'html');
app.engine('html', ejs.renderFile);
// give ejs access to some interesting information
app.use(function(req, res, next){
app.use(function(req: Request, res: any, next: any){
res.locals.res = res;
res.locals.req = req;
res.locals.masterPlugins = masterPlugins;
res.locals.slaves = slaves;
res.locals.slaves = db.slaves;
res.locals.moment = moment;
next();
});
@@ -124,7 +136,7 @@ if(!config.disablePrometheusPushgateway){
setInterval(() => {
registerMoreMetrics();
pushgateway.push({ jobName: 'clusterio', groupings: {instance: config.publicIP + ":" + config.masterPort, owner: config.username}}, function(err, resp, body) {})
}, 15000)
}, 5000)
}
// collect express request durations ms
@@ -169,13 +181,18 @@ const prometheusUPSGauge = new Prometheus.Gauge({
help: 'UPS of the current server',
labelNames: ["instanceID", "instanceName"],
});
const prometheusTickGauge = new Prometheus.Gauge({
name: prometheusPrefix+'tick_gauge',
help: 'Servers current gametick',
labelNames: ["instanceID", "instanceName"],
});
const prometheusMasterInventoryGauge = new Prometheus.Gauge({
name: prometheusPrefix+'master_inventory_gauge',
help: 'Amount of items stored on master',
labelNames: ["itemName"],
});
setInterval(()=>{
fs.writeFileSync("database/items.json", JSON.stringify(db.items));
fs.writeFileSync("database/items.json", JSON.stringify(db.items));
},config.autosaveInterval || 60000);
/**
GET Prometheus metrics endpoint. Returns performance and usage metrics in a prometheus readable format.
@@ -186,22 +203,22 @@ GET Prometheus metrics endpoint. Returns performance and usage metrics in a prom
app.get('/metrics', (req, res) => {
endpointHitCounter.labels(req.route.path).inc();
res.set('Content-Type', Prometheus.register.contentType);
/// gather some static metrics
registerMoreMetrics();
res.end(Prometheus.register.metrics());
});
function registerMoreMetrics(){
for(let instanceID in slaves){
for(let instanceID in db.slaves){
// playercount
try{
prometheusPlayerCountGauge.labels(instanceID, slaves[instanceID].instanceName).set(Number(slaves[instanceID].playerCount) || 0);
prometheusPlayerCountGauge.labels(instanceID, db.slaves[instanceID].instanceName).set(Number(db.slaves[instanceID].playerCount) || 0);
}catch(e){}
// UPS
try{
prometheusUPSGauge.labels(instanceID, slaves[instanceID].instanceName).set(Number(slaves[instanceID].meta.UPS) || 60);
if(slaves[instanceID].meta.tick && typeof slaves[instanceID].meta.tick === "number") prometheusUPSGauge.labels(instanceID, slaves[instanceID].instanceName).set(Number(slaves[instanceID].meta.tick) || 0);
prometheusUPSGauge.labels(instanceID, db.slaves[instanceID].instanceName).set(Number(db.slaves[instanceID].meta.UPS) || 60);
if(db.slaves[instanceID].meta.tick && typeof db.slaves[instanceID].meta.tick === "number") prometheusUPSGauge.labels(instanceID, db.slaves[instanceID].instanceName).set(Number(db.slaves[instanceID].meta.tick) || 0);
}catch(e){}
}
// inventory
@@ -212,58 +229,15 @@ function registerMoreMetrics(){
}
// Slave count
let numberOfActiveSlaves = 0;
for(let instance in slaves){
if(Date.now() - Number(slaves[instance].time) < 1000 * 30) numberOfActiveSlaves++;
for(let instance in db.slaves){
if(Date.now() - Number(db.slaves[instance].time) < 1000 * 30) numberOfActiveSlaves++;
}
prometheusConnectedInstancesCounter.set(numberOfActiveSlaves);
}
// set up database
const db = {};
const db: db = getDatabase(config);
(function(){
try{
let x = fs.statSync(path.resolve(config.databaseDirectory, "slaves.json"));
console.log(`loading slaves from path.resolve(config.databaseDirectory, "slaves.json")`);
slaves = JSON.parse(fs.readFileSync(path.resolve(config.databaseDirectory, "slaves.json")));
} catch (e){
slaves = {};
}
try{
x = fs.statSync(path.resolve(config.databaseDirectory, "items.json"));
console.log(`loading items from ${path.resolve(config.databaseDirectory, "items.json")}`);
db.items = JSON.parse(fs.readFileSync(path.resolve(config.databaseDirectory, "items.json")));
} catch (e){
db.items = {};
}
})();
db.items.addItem = function(object) {
if(object.name == "addItem" || object.name == "removeItem") {
console.error("Fuck you, that would screw everything up if you named your item that.");
return false;
} else {
if(this[object.name] && Number(this[object.name]) != NaN){
this[object.name] = Number(this[object.name]) + Number(object.count);
} else {
this[object.name] = Number(object.count);
}
return true;
}
};
db.items.removeItem = function(object) {
if(object.name == "addItem" || object.name == "removeItem") {
console.error("Fuck you, that would screw everything up if you named your item that.");
return false;
} else {
if(this[object.name] && Number(this[object.name]) != NaN){
this[object.name] = Number(this[object.name]) - Number(object.count);
} else {
this[object.name] = 0;
}
return true;
}
};
// store slaves and inventory in a .json full of JSON data
process.on('SIGINT', shutdown); // ctrl + c
@@ -271,35 +245,29 @@ process.on('SIGHUP', shutdown); // terminal closed
async function shutdown() {
console.log('Ctrl-C...');
let exitStartTime = Date.now();
try {
// set insane limit to slave length, if its longer than this we are probably being ddosed or something
if(slaves && Object.keys(slaves).length < 2000000){
console.log("saving to slaves.json");
fs.writeFileSync(path.resolve(config.databaseDirectory, "slaves.json"), JSON.stringify(slaves));
} else if(slaves) {
console.log("Slave database too large, not saving ("+Object.keys(slaves).length+")");
}
if(db.items && Object.keys(db.items).length < 50000){
console.log("saving to items.json");
fs.writeFileSync(path.resolve(config.databaseDirectory, "items.json"), JSON.stringify(db.items));
} else if(slaves) {
console.log("Item database too large, not saving ("+Object.keys(slaves).length+")");
}
for(let i in masterPlugins){
let plugin = masterPlugins[i];
if(plugin.main && plugin.main.onExit && typeof plugin.main.onExit == "function"){
let startTime = Date.now();
await plugin.main.onExit();
console.log("Plugin "+plugin.pluginConfig.name+" exited in "+(Date.now()-startTime)+"ms");
}
}
console.log("Clusterio cleanly exited in "+(Date.now()-exitStartTime)+"ms");
process.exit(0);
} catch(e) {
console.log(e);
console.log("Clusterio failed to exit cleanly. Time elapsed: "+(Date.now()-exitStartTime)+"ms");
process.exit(1)
// set insane limit to slave length, if its longer than this we are probably being ddosed or something
if(db.slaves && Object.keys(db.slaves).length < 2000000){
console.log("saving to slaves.json");
fs.writeFileSync(path.resolve(config.databaseDirectory, "slaves.json"), JSON.stringify(db.slaves));
} else if(db.slaves) {
console.log("Slave database too large, not saving ("+Object.keys(db.slaves).length+")");
}
if(db.items && Object.keys(db.items).length < 50000){
console.log("saving to items.json");
fs.writeFileSync(path.resolve(config.databaseDirectory, "items.json"), JSON.stringify(db.items));
} else if(db.slaves) {
console.log("Item database too large, not saving ("+Object.keys(db.slaves).length+")");
}
for(let i in masterPlugins){
let plugin = masterPlugins[i];
if(plugin.main && plugin.main.onExit && typeof plugin.main.onExit == "function"){
let startTime = Date.now();
await plugin.main.onExit();
console.log("Plugin "+plugin.pluginConfig.name+" exited in "+(Date.now()-startTime)+"ms");
}
}
console.log("Clusterio cleanly exited in "+(Date.now()-exitStartTime)+"ms");
process.exit(0);
}
/**
@@ -308,7 +276,7 @@ POST World ID management. Slaves post here to tell the server they exist
@instance
@alias /api/getID
*/
app.post("/api/getID", authenticate.middleware, function(req,res) {
app.post("/api/getID", authenticate.middleware, function(req, res) {
endpointHitCounter.labels(req.route.path).inc();
// request.body should be an object
// {rconPort, rconPassword, serverPort, mac, time}
@@ -318,12 +286,12 @@ app.post("/api/getID", authenticate.middleware, function(req,res) {
if(debug){
console.log(req.body)
}
if(!slaves[req.body.unique]){
slaves[req.body.unique] = {};
if(!db.slaves[req.body.unique]){
db.slaves[req.body.unique] = {};
}
for(k in req.body){
for(let k in req.body){
if(k != "meta" && req.body.hasOwnProperty(k)){
slaves[req.body.unique][k] = req.body[k];
db.slaves[req.body.unique][k] = req.body[k];
}
}
// console.log("Slave registered: " + slaves[req.body.unique].mac + " : " + slaves[req.body.unique].serverPort+" at " + slaves[req.body.unique].publicIP + " with name " + slaves[req.body.unique].instanceName);
@@ -339,19 +307,17 @@ app.post("/api/editSlaveMeta", authenticate.middleware, function(req,res) {
endpointHitCounter.labels(req.route.path).inc();
// request.body should be an object
// {instanceID, pass, meta:{x,y,z}}
if(req.body && req.body.instanceID && req.body.meta){
if(slaves[req.body.instanceID]){
if(!slaves[req.body.instanceID].meta) {
slaves[req.body.instanceID].meta = {};
if(req.body && req.body.instanceID && req.body.password && req.body.meta){
// check for editing permissions
if(db.slaves[req.body.instanceID] && db.slaves[req.body.instanceID].rconPassword == req.body.password){
if(!db.slaves[req.body.instanceID].meta){
db.slaves[req.body.instanceID].meta = {};
}
slaves[req.body.instanceID].meta = deepmerge(slaves[req.body.instanceID].meta, req.body.meta, {clone:true});
db.slaves[req.body.instanceID].meta = deepmerge(db.slaves[req.body.instanceID].meta, req.body.meta, {clone:true});
let metaPortion = JSON.stringify(req.body.meta);
if(metaPortion.length > 50) {
metaPortion = metaPortion.substring(0,20) + "...";
}
if(metaPortion.length > 50) metaPortion = metaPortion.substring(0,20) + "...";
// console.log("Updating slave "+slaves[req.body.instanceID].instanceName+": " + slaves[req.body.instanceID].mac + " : " + slaves[req.body.instanceID].serverPort+" at " + slaves[req.body.instanceID].publicIP, metaPortion);
res.sendStatus(200);
} else {
res.send("ERROR: Invalid instanceID or password")
}
@@ -367,20 +333,12 @@ POST Get metadata from a single slave. For whenever you find the data returned b
app.post("/api/getSlaveMeta", function (req, res) {
endpointHitCounter.labels(req.route.path).inc();
console.log("body", req.body);
if(req.body && req.body.instanceID){
if (slaves[req.body.instanceID]) {
console.log("returning meta for ", req.body.instanceID);
if(!slaves[req.body.instanceID].meta) {
slaves[req.body.instanceID].meta = {};
}
res.send(JSON.stringify(slaves[req.body.instanceID].meta))
} else {
res.status(404);
res.send('{"status": 404, "info": "Slave not registered"}')
}
if(req.body && req.body.instanceID && req.body.password){
console.log("returning meta for ", req.body.instanceID);
res.send(JSON.stringify(db.slaves[req.body.instanceID].meta))
} else {
res.status(400);
res.send('{"INVALID_REQUEST":1}');
res.status(400);
res.send('{"INVALID REQUEST":1}');
}
});
// mod management
@@ -391,11 +349,11 @@ POST Check if a mod has been uploaded to the master before. Only checks against
@instance
@alias /api/checkMod
*/
app.post("/api/checkMod", authenticate.middleware, function(req,res) {
app.post("/api/checkMod", function(req,res) {
endpointHitCounter.labels(req.route.path).inc();
let files = fs.readdirSync(masterModFolder);
let files: string[] = fs.readdirSync(masterModFolder);
let found = false;
files.forEach(file => {
files.forEach((file) => {
if(file == req.body.modName) {
found = true;
}
@@ -418,7 +376,7 @@ app.post("/api/uploadMod", authenticate.middleware, function(req,res) {
endpointHitCounter.labels(req.route.path).inc();
if (req.files && req.files.file) {
// console.log(req.files.file);
req.files.file.mv(path.resolve(config.databaseDirectory, "masterMods", req.files.file.name), function(err) {
req.files.file.mv(path.resolve(config.databaseDirectory, "masterMods", req.files.file.name), function(err: Error) {
if (err) {
res.status(500).send(err);
} else {
@@ -428,63 +386,41 @@ app.post("/api/uploadMod", authenticate.middleware, function(req,res) {
});
} else {
res.send('No files were uploaded.');
}
return;
}
});
/**
Prepare the slaveCache for both /api/slaves calls
*/
let slaveCache = {
timestamp: Date.now(),
};
function getSlaves() {
if(!slaveCache.cache || Date.now() - slaveCache.timestamp > 5000) {
let copyOfSlaves = JSON.parse(JSON.stringify(slaves));
slaveCache.cache = copyOfSlaves;
slaveCache.timestamp = Date.now();
}
return slaveCache;
}
/**
GET endpoint for getting information about all our slaves
@memberof clusterioMaster
@instance
@alias /api/slaves
*/
let slaveCache: {timestamp:number, cache?:Slaves} = {
timestamp: Date.now(),
};
app.get("/api/slaves", function(req, res) {
endpointHitCounter.labels(req.route.path).inc();
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
let slaveCache = getSlaves();
for(key in slaveCache.cache) {
slaveCache.cache[key].rconPassword = "hidden";
}
res.send(slaveCache.cache);
});
/**
POST endpoint for getting the rconPasswords of all our slaves
@memberof clusterioMaster
@instance
@alias /api/rconPasswords
*/
app.post("/api/rconPasswords", authenticate.middleware, function(req, res) {
endpointHitCounter.labels(req.route.path).inc();
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
let slaveCache = getSlaves();
let validKeys = ['rconPort', 'rconPassword', 'publicIP'];
for(key in slaveCache.cache) {
slaveCache.cache[key] = {'publicIP': slaveCache.cache[key].publicIP, 'rconPort': slaveCache.cache[key].rconPort, 'rconPassword': slaveCache.cache[key].rconPassword};
if(!slaveCache.cache || Date.now() - slaveCache.timestamp > 5000){
let copyOfSlaves: Slaves = JSON.parse(JSON.stringify(db.slaves));
// filter out the rcon password because thats kindof not a safe thing to share
for(let key in copyOfSlaves){
copyOfSlaves[key].rconPassword = "hidden";
}
slaveCache.cache = copyOfSlaves;
slaveCache.timestamp = Date.now();
}
res.send(slaveCache.cache);
});
var recievedItemStatisticsBySlaveID: any = {};
var sentItemStatisticsBySlaveID: any = {};
/*
var recievedItemStatistics = new averagedTimeSeries({
maxEntries: config.itemStats.maxEntries,
entriesPerSecond: config.itemStats.entriesPerSecond,
}, console.log);*/
var recievedItemStatisticsBySlaveID = {};
var sentItemStatisticsBySlaveID = {};
//
/**
POST endpoint for storing items in master's inventory.
@@ -501,7 +437,7 @@ app.post("/api/place", authenticate.middleware, function(req, res) {
endpointHitCounter.labels(req.route.path).inc();
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
let x;
let x: item;
try {
x = JSON.parse(req.body);
} catch (e) {
@@ -512,8 +448,8 @@ app.post("/api/place", authenticate.middleware, function(req, res) {
}
if(!x.instanceID) {
x.instanceID = "unknown";
Object.keys(slaves).forEach(instanceID => {
if(slaves[instanceID].instanceName == req.body.instanceName) {
Object.keys(db.slaves).forEach(instanceID => {
if(db.slaves[instanceID].instanceName == req.body.instanceName) {
x.instanceID = instanceID;
}
});
@@ -547,20 +483,13 @@ app.post("/api/place", authenticate.middleware, function(req, res) {
name:req.body.name,
count:req.body.count
});
// Attempt confirming
res.send("success");
} else {
res.send("failure");
}
});
const routes_api_remove = require("./routes/api/remove.js");
var neuralDole = null;
if(config.useNeuralNetDoleDivider)//Only initialize neural network when it's enabled, otherwise it might override gauge
neuralDole=new routes_api_remove.neuralDole({
items: db.items, gaugePrefix: prometheusPrefix
});
/**
POST endpoint to remove items from DB when client orders items.
@@ -572,8 +501,11 @@ POST endpoint to remove items from DB when client orders items.
@param {string} [itemStack.instanceName="unknown"] the name of an instance for identification in statistics, as provided when launching it. ex node client.js start [name]
@returns {itemStack} the number of items actually removed, may be lower than what was asked for due to shortages.
*/
var _doleDivisionFactor: {[name:string]: number} = {}; //If the server regularly can't fulfill requests, this number grows until it can. Then it slowly shrinks back down.
app.post("/api/remove", authenticate.middleware, function(req, res) {
endpointHitCounter.labels(req.route.path).inc();
const doleDivisionRetardation = 10; //lower rates will equal more dramatic swings
const maxDoleDivision = 250; //a higher cap will divide the store more ways, but will take longer to recover as supplies increase
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
// save items we get
@@ -584,10 +516,10 @@ app.post("/api/remove", authenticate.middleware, function(req, res) {
if(!object.instanceName) {
object.instanceName = "unknown";
}
if(slaves[object.instanceID]) object.instanceName = slaves[object.instanceID].instanceName;
let item = db.items[object.name];
if(db.slaves[object.instanceID]) object.instanceName = db.slaves[object.instanceID].instanceName;
let item = db.items[object.name]
// console.dir(doc);
if((item === undefined)
if(!item
||((
config.disableImportsOfEverythingExceptElectricity === true || config.disableImportsOfEverythingExceptElectricity === "true" )
&& object.name != "electricity")
@@ -597,11 +529,10 @@ app.post("/api/remove", authenticate.middleware, function(req, res) {
}
res.send({name:object.name, count:0});
} else if(config.disableFairItemDistribution){
// Give out as much items as possible until there are 0 left. This might lead to one slave getting all the items and the rest nothing.
let numberToRemove = Math.min(Math.abs(Number(object.count)),Number(item));
db.items.removeItem({count: numberToRemove, name: object.name});
res.send({count: numberToRemove, name: object.name});
// track statistics and do graphing things
prometheusImportGauge.labels(object.instanceID, object.name).inc(Number(numberToRemove) || 0);
let sentItemStatistics = sentItemStatisticsBySlaveID[object.instanceID];
@@ -618,27 +549,45 @@ app.post("/api/remove", authenticate.middleware, function(req, res) {
});
//console.log(sentItemStatistics.data)
sentItemStatisticsBySlaveID[object.instanceID] = sentItemStatistics;
} else if(config.useNeuralNetDoleDivider){
// use fancy neural net to calculate a "fair" dole division rate.
neuralDole.divider({
res,
object,
config,
sentItemStatisticsBySlaveID,
prometheusImportGauge
})
} else {
// Use dole division. Makes it really slow to drain out the last little bit.
routes_api_remove.doleDivider({
item,
object,
db,
sentItemStatisticsBySlaveID,
config,
prometheusDoleFactorGauge,
prometheusImportGauge,
req,res,
})
const originalCount = Number(object.count) || 0;
object.count /= ((_doleDivisionFactor[object.name]||0)+doleDivisionRetardation)/doleDivisionRetardation;
object.count = Math.round(object.count);
if(item.length > 40) console.info(`Serving ${object.count}/${originalCount} ${object.name} from ${item} ${object.name} with dole division factor ${(_doleDivisionFactor[object.name]||0)} (real=${((_doleDivisionFactor[object.name]||0)+doleDivisionRetardation)/doleDivisionRetardation}), item is ${Number(item) > Number(object.count)?'stocked':'short'}.`);
// Update existing items if item name already exists
if(Number(item) > Number(object.count)) {
//If successful, increase dole
_doleDivisionFactor[object.name] = Math.max((_doleDivisionFactor[object.name]||0)||1, 1) - 1;
if(config.logItemTransfers){
console.log("removed: " + object.name + " " + object.count + " . " + item + " and sent to " + object.instanceID + " | " + object.instanceName);
}
if(db.items.removeItem({count: object.count, name: object.name})){
let sentItemStatistics = sentItemStatisticsBySlaveID[object.instanceID];
if(sentItemStatistics === undefined){
sentItemStatistics = new averagedTimeSeries({
maxEntries: config.itemStats.maxEntries,
entriesPerSecond: config.itemStats.entriesPerSecond,
mergeMode: "add",
}, console.log);
}
sentItemStatistics.add({
key:object.name,
value:object.count,
});
//console.log(sentItemStatistics.data)
sentItemStatisticsBySlaveID[object.instanceID] = sentItemStatistics;
}
prometheusDoleFactorGauge.labels(object.name).set(_doleDivisionFactor[object.name] || 0);
prometheusImportGauge.labels(object.instanceID, object.name).inc(Number(object.count) || 0);
res.send({count: object.count, name: object.name});
} else {
// if we didn't have enough, attempt giving out a smaller amount next time
_doleDivisionFactor[object.name] = Math.min(maxDoleDivision, Math.max((_doleDivisionFactor[object.name]||0)||1, 1) * 2);
res.send({name:object.name, count:0});
//console.log('failure out of ' + object.name + " | " + object.count + " from " + object.instanceID + " ("+object.instanceName+")");
}
}
});
@@ -685,24 +634,24 @@ app.get("/api/inventoryAsObject", function(req, res) {
@memberof clusterioMaster
@instance
@alias api/modData
@alias api/modmeta
@returns {object} JSON
*/
// wrap a request in an promise
async function downloadPage(url) {
return new Promise((resolve, reject) => {
request(url, (error, response, body) => {
resolve(body);
});
async function downloadPage(url: string): Promise<object> {
return new Promise((resolve, reject) => {
request(url, (_error: any, _response: any, body: object) => {
resolve(body);
});
}
});
}
app.get("/api/modmeta", async function(req, res) {
endpointHitCounter.labels(req.route.path).inc();
res.header("Access-Control-Allow-Origin", "*");
res.setHeader('Content-Type', 'application/json');
let modData = await downloadPage("https://mods.factorio.com/api/mods/" + req.query.modname);
res.header("Access-Control-Allow-Origin", "*");
res.setHeader('Content-Type', 'application/json');
let modData = await downloadPage("https://mods.factorio.com/api/mods/" + req.query.modname);
res.send(modData);
});
@@ -717,38 +666,45 @@ Requires x-access-token header to be set. Find you api token in secret-api-token
@param {object} JSON {instanceID:19412312, broadcast:false, command:"/c game.print('hello')"}
@returns {object} Status {auth: bool, message: "Informative error", data:{}}
*/
app.post("/api/runCommand", authenticate.middleware, function(req,res) {
app.post("/api/runCommand", (req,res) => {
endpointHitCounter.labels(req.route.path).inc();
// validate request.body
let body = req.body;
var token = req.headers['x-access-token'];
if(!token) return res.status(401).send({ auth: false, message: 'No token provided.' });
jwt.verify(token, config.masterAuthSecret, function(err: Error) {
if(err) return res.status(500).send({ auth: false, message: 'Failed to authenticate token.' });
// validate request.body
let body = req.body;
if (
body.broadcast
&& body.command
&& typeof body.command == "string"
&& body.command[0] == "/")
{
let instanceResponses = [];
for (let instanceID in wsSlaves) {
// skip loop if the property is from prototype
if (!wsSlaves.hasOwnProperty(instanceID)) continue;
wsSlaves[instanceID].runCommand(body.command);
}
res.status(200).send({auth: true, message: "success", response: "Cluster wide messaging initiated"});
} else if (
body.instanceID
&& wsSlaves[body.instanceID]
&& body.command
&& typeof body.command == "string"
&& body.command[0] == "/")
{
// execute command
wsSlaves[body.instanceID].runCommand(body.command, data => {
res.status(200).send({auth: true, message: "success", data});
});
} else {
res.status(400).send({auth: true, message: "Error: invalid request.body"});
}
if (
body.broadcast
&& body.command
&& typeof body.command == "string"
&& body.command[0] == "/")
{
let instanceResponses = [];
for (let instanceID in wsSlaves) {
// skip loop if the property is from prototype
if (!wsSlaves.hasOwnProperty(instanceID)) continue;
wsSlaves[instanceID].runCommand(body.command);
}
res.status(200).send({auth: true, message: "success", response: "Cluster wide messaging initiated"});
} else if (
body.instanceID
&& wsSlaves[body.instanceID]
&& body.command
&& typeof body.command == "string"
&& body.command[0] == "/")
{
// execute command
wsSlaves[body.instanceID].runCommand(body.command, (data: string) => {
res.status(200).send({auth: true, message: "success", data});
});
} else {
res.status(400).send({auth: true, message: "Error: invalid request.body"});
}
});
});
/**
GET endpoint. Returns factorio's base locale as a JSON object.
@@ -760,7 +716,7 @@ GET endpoint. Returns factorio's base locale as a JSON object.
*/
app.get("/api/getFactorioLocale", function(req,res){
endpointHitCounter.labels(req.route.path).inc();
getFactorioLocale.asObject(config.factorioDirectory, "en", (err, factorioLocale) => {
getFactorioLocale.asObject(config.factorioDirectory, "en", (_err, factorioLocale) => {
res.send(factorioLocale);
});
});
@@ -769,7 +725,7 @@ if(args.sslPort || config.sslPort){
var certificate = fs.readFileSync(path.resolve(config.databaseDirectory, 'certificates/cert.crt'));
var privateKey = fs.readFileSync(path.resolve(config.databaseDirectory, 'certificates/cert.key'));
httpsServer = require("https").createServer({
var httpsServer = require("https").createServer({
key: privateKey,
cert: certificate
}, app).listen(args.sslPort || config.sslPort);
@@ -778,24 +734,27 @@ if(args.sslPort || config.sslPort){
}
}
if(args.masterPort || config.masterPort){
server = require("http").Server(app);
var server = require("http").Server(app);
server.listen(args.masterPort || config.masterPort || 8080, function () {
console.log("Listening on port %s...", server.address().port);
});
module.exports = server;
}
/* Websockets for remoteMap */
var io = require("socket.io")();
var io = require("socket.io")(server);
const ioMetrics = require("socket.io-prometheus");
ioMetrics(io);
var slaveMappers = {};
class slaveMapper {
instanceID: string
socket: any
lastBeat: number
constructor(instanceID, socket) {
this.instanceID = instanceID;
this.socket = socket;
this.lastBeat = Date.now();
this.socket.on("heartbeat", () => {
prometheusWsUsageCounter.labels('heartbeat', this.instanceID).inc();
// we aren't ready to die yet apparently
@@ -810,7 +769,7 @@ class slaveMapper {
prometheusWsUsageCounter.labels('sendEntity', this.instanceID).inc();
Object.keys(mapRequesters).forEach(requesterName => {
let requester = mapRequesters[requesterName];
if(requester.instanceID == this.instanceID){
// this mapRequester is listening to this slaveMapper, so we send it updates
requester.socket.emit("displayEntity", entity);
@@ -819,16 +778,18 @@ class slaveMapper {
});
}
}
var mapRequesters = {};
class mapRequester {
requesterID: string
socket: any
instanceID: string
lastBeat: number
constructor(requesterID, socket, instanceID){
this.requesterID = requesterID;
this.socket = socket;
this.instanceID = instanceID;
this.lastBeat = Date.now();
this.socket.on("heartbeat", () => {
prometheusWsUsageCounter.labels('heartbeat', "other").inc();
// we aren't ready to die yet apparently
@@ -860,16 +821,20 @@ class mapRequester {
});
}
}
/* Websockets for send and recieve combinators */
var wsSlaves = {};
var wsChatRecievers = [];
var wsAlertRecievers = [];
class wsSlave {
instanceID: string
socket: any
lastBeat: number
commandsWaitingForReturn: object
constructor(instanceID, socket){
this.instanceID = instanceID;
this.socket = socket;
this.lastBeat = Date.now();
this.socket.on("heartbeat", () => {
prometheusWsUsageCounter.labels('heartbeat', this.instanceID).inc();
this.lastBeat = Date.now();
@@ -891,24 +856,24 @@ class wsSlave {
delete this.commandsWaitingForReturn[resp.commandID];
}
});
this.socket.on("gameChat", data => {
prometheusWsUsageCounter.labels('gameChat', this.instanceID).inc();
if(typeof data === "object"){
data.timestamp = Date.now();
if(!global.wsChatRecievers) global.wsChatRecievers = [];
global.wsChatRecievers.forEach(socket => {
if(!wsChatRecievers) wsChatRecievers = [];
wsChatRecievers.forEach(socket => {
socket.emit("gameChat", data);
});
}
});
this.socket.on("alert", alert => {
prometheusWsUsageCounter.labels('alert', this.instanceID).inc();
if(typeof alert === "object"){
alert.timestamp = Date.now();
if(!global.wsAlertRecievers) global.wsAlertRecievers = [];
global.wsAlertRecievers.forEach(socket => {
if(!wsAlertRecievers) wsAlertRecievers = [];
wsAlertRecievers.forEach(socket => {
socket.emit("alert", alert);
});
}
@@ -920,19 +885,6 @@ class wsSlave {
if(commandID) this.commandsWaitingForReturn[commandID] = {callback, timestamp: Date.now()};
}
}
io.use((socket, next) => {
let token = socket.handshake.query.token;
authenticate.check(token).then((result) => {
if (result.ok) {
next();
} else {
console.error(`SOCKET | authentication failed for ${socket.handshake.address}`);
next(new Error(result.msg));
}
});
});
io.on('connection', function (socket) {
// cleanup dead sockets from disconnected people
let terminatedConnections = 0;
@@ -947,10 +899,10 @@ io.on('connection', function (socket) {
});
});
if(terminatedConnections > 0) console.log("SOCKET | There are currently "+currentConnections+" websocket connections, deleting "+terminatedConnections+" on timeout");
// tell our friend that we are listening
setTimeout(()=>socket.emit('hello', { hello: 'world' }), 5000);
/* initial processing for remoteMap */
socket.on('registerSlaveMapper', function (data) {
prometheusWsUsageCounter.labels('registerSlaveMapper', "other").inc();
@@ -965,7 +917,7 @@ io.on('connection', function (socket) {
socket.emit("mapRequesterReady", true);
console.log("remoteMap | SOCKET registered map requester for "+data.instanceID);
});
/* Websockets for send and recieve combinators */
socket.on("registerSlave", function(data) {
prometheusWsUsageCounter.labels('registerSlave', "other").inc();
@@ -976,20 +928,27 @@ io.on('connection', function (socket) {
});
socket.on("registerChatReciever", function(data){
prometheusWsUsageCounter.labels("registerChatReciever", "other").inc();
if(!global.wsChatRecievers) global.wsChatRecievers = [];
global.wsChatRecievers.push(socket);
if(!wsChatRecievers) wsChatRecievers = [];
wsChatRecievers.push(socket);
});
socket.on("registerAlertReciever", function(data){
prometheusWsUsageCounter.labels("registerAlertReciever", "other").inc();
if(!global.wsAlertRecievers) global.wsAlertRecievers = [];
global.wsAlertRecievers.push(socket);
if(!wsAlertRecievers) wsAlertRecievers = [];
wsAlertRecievers.push(socket);
});
});
// handle plugins on the master
async function getPlugins(){
pluginManagement();
var masterPlugins: MasterPlugin[] = [];
async function pluginManagement(){
let startPluginLoad = Date.now();
const pluginManager = require("lib/manager/pluginManager.js")(config);
masterPlugins = await getPlugins();
console.log("All plugins loaded in "+(Date.now() - startPluginLoad)+"ms");
return;
}
async function getPlugins(): Promise<MasterPlugin[]>{
const pluginManager = require("./lib/manager/pluginManager.js")(config);
let plugins = [];
let pluginsToLoad = await pluginManager.getPlugins();
for(let i = 0; i < pluginsToLoad.length; i++){
@@ -1010,7 +969,7 @@ async function getPlugins(){
}),
pluginConfig,
});
console.log("Loaded plugin "+pluginConfig.name+" in "+(Date.now() - pluginStartedLoading)+"ms");
}
}
@@ -1019,15 +978,8 @@ async function getPlugins(){
let plugin = plugins[i];
if(plugin.main.onLoadFinish && typeof plugin.main.onLoadFinish == "function") await plugin.main.onLoadFinish({plugins});
}
console.log("All plugins loaded in "+(Date.now() - startPluginLoad)+"ms");
return plugins;
}
masterPlugins = await getPlugins();
io.attach(server);
module.exports = app;
})();
}
BIN
View File
Binary file not shown.
File renamed without changes.
File renamed without changes.
@@ -32,7 +32,7 @@ module.exports = function(config){
}
} else {
return async function thisAintGonnaWork(){
let err = new Error("getInstanceName from lib/clusterTools needs to be called with {config} to work");
let err = new Error("getInstanceName from _app/clusterTools needs to be called with {config} to work");
console.log(err);
throw err;
}
File renamed without changes.
File renamed without changes.
+3 -2
View File
@@ -15,8 +15,9 @@ const child_process = require("child_process");
const path = require("path");
const mkdirp = require("mkdirp");
const appRootPath = require("app-root-path");
const fileOps = require("lib/fileOps");
const fileOps = require("_app/fileOps");
module.exports = class factorioServer {
constructor({
@@ -164,4 +165,4 @@ module.exports = class factorioServer {
});
});
}
}
}
@@ -2,6 +2,7 @@ const assert = require("assert");
const fs = require("fs");
const path = require("path");
const appRootPath = require("app-root-path");
const parallel = require("mocha.parallel");
const mkdirp = require("mkdirp");
@@ -74,7 +75,7 @@ describe("class factorioServer/index.js", ()=>{
describe(".createMap(mapName.zip)", ()=>{
it("creates a map file as instanceDirectory/saves/map.zip", async function(){
this.timeout(20000); // these test often takes a lot of time in the map creation step (4500 ms on i3 dev system)
let relativePath = path.relative('.', __dirname);
let relativePath = __dirname.replace(appRootPath, "").substr(1);
let server = new factorioServer({
instanceDirectory: path.join(relativePath, "_testData_createMap"),
instanceName: "Unit testing instance",
@@ -92,7 +93,7 @@ describe("class factorioServer/index.js", ()=>{
});
it("supports custom filenames", async function(){
this.timeout(20000);
let relativePath = path.relative('.', __dirname);
let relativePath = __dirname.replace(appRootPath, "").substr(1);
let server = new factorioServer({
instanceDirectory: path.join(relativePath, "_testData_createMap_2"),
instanceName: "Unit testing instance",
Loaded 100 of 141 files, more files were not shown because too many files have changed in this diff. Show more