mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
Compare commits
89
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d819644b01 | ||
|
|
042230dc2c | ||
|
|
a00183e94c | ||
|
|
c95d9f9464 | ||
|
|
a1237d8cfd | ||
|
|
95d06293f5 | ||
|
|
2714b09e5d | ||
|
|
e733cfde03 | ||
|
|
008ea139c6 | ||
|
|
3338c439dd | ||
|
|
b956438d8d | ||
|
|
cdbbdd41c7 | ||
|
|
e2775adcc3 | ||
|
|
d909b4aa47 | ||
|
|
49611a2062 | ||
|
|
ee6ab6ab72 | ||
|
|
de92ca96b9 | ||
|
|
77281d0662 | ||
|
|
2831c07000 | ||
|
|
6ac8c0711f | ||
|
|
c9cbc48d95 | ||
|
|
50370bf858 | ||
|
|
d1fd4c3565 | ||
|
|
8672afe940 | ||
|
|
adf6763a0d | ||
|
|
d48a824a69 | ||
|
|
f28b73cf31 | ||
|
|
fec3cf5ddf | ||
|
|
c7cfb8ce70 | ||
|
|
93c210f0c7 | ||
|
|
251ec696a3 | ||
|
|
773a2c412e | ||
|
|
c0499e7f27 | ||
|
|
0a65946bf4 | ||
|
|
233410add7 | ||
|
|
fea9fb28ca | ||
|
|
e1b510b441 | ||
|
|
34301eb381 | ||
|
|
4a0956d174 | ||
|
|
3b814eeea4 | ||
|
|
98c6795f8f | ||
|
|
21dd80df9b | ||
|
|
0b34f07954 | ||
|
|
c978e8e748 | ||
|
|
71870daaba | ||
|
|
5a60f317ba | ||
|
|
b666a909ab | ||
|
|
0afe35b67c | ||
|
|
096ead5bc4 | ||
|
|
fa4c10dce2 | ||
|
|
1f8ac50950 | ||
|
|
10d5b0da9b | ||
|
|
6c32271c56 | ||
|
|
a122f2b09e | ||
|
|
f35be2f355 | ||
|
|
d66d22a9cd | ||
|
|
7d6a5bb6d2 | ||
|
|
bf2e78d530 | ||
|
|
bf4b7fe1b8 | ||
|
|
66ba706c56 | ||
|
|
79c9ac839e | ||
|
|
7f59b200b1 | ||
|
|
d719a73328 | ||
|
|
616bb9cc8f | ||
|
|
3ae55cbd27 | ||
|
|
7ae9ff6649 | ||
|
|
0b3007d22d | ||
|
|
bd7a116860 | ||
|
|
0c9ed582cc | ||
|
|
d7bfc2193e | ||
|
|
ce9e1d876a | ||
|
|
86d4f3dba4 | ||
|
|
0fe5633926 | ||
|
|
595b4e5ac9 | ||
|
|
7d2ac5808f | ||
|
|
fddafaec09 | ||
|
|
abf4b5d926 | ||
|
|
00dfae8bd1 | ||
|
|
2ef6c93b05 | ||
|
|
2165acb171 | ||
|
|
8357fa23ce | ||
|
|
94d434c8ef | ||
|
|
56cf131c70 | ||
|
|
a2be560b8b | ||
|
|
a3e33f6164 | ||
|
|
01e96a4dcb | ||
|
|
d189a82494 | ||
|
|
c15709ddc1 | ||
|
|
3f29a722bb |
No files matched your search
+3
-3
@@ -22,6 +22,7 @@ coverage
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
dist/
|
||||
|
||||
node_modules/*
|
||||
!node_modules/_app
|
||||
@@ -64,6 +65,5 @@ sharedPlugins/*/DISABLED
|
||||
|
||||
\.cache-require-paths\.json
|
||||
|
||||
\.vs/
|
||||
|
||||
bin/
|
||||
# IDE
|
||||
.idea/
|
||||
@@ -1,8 +1,45 @@
|
||||
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
|
||||
version: v1.12.0
|
||||
version: v1.13.3
|
||||
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'
|
||||
'npm:hawk:20160119':
|
||||
- node-factorio-api > documentation > highlight.js > gear-lib > less > request > hawk:
|
||||
patched: '2019-02-25T06:48:21.845Z'
|
||||
'npm:http-signature:20150122':
|
||||
- node-factorio-api > documentation > highlight.js > gear-lib > less > request > http-signature:
|
||||
patched: '2019-02-25T06:48:21.845Z'
|
||||
'npm:mime:20170907':
|
||||
- node-factorio-api > documentation > highlight.js > gear-lib > mime:
|
||||
patched: '2019-02-25T06:48:21.845Z'
|
||||
- node-factorio-api > documentation > highlight.js > gear-lib > less > mime:
|
||||
patched: '2019-02-25T06:48:21.845Z'
|
||||
- node-factorio-api > documentation > highlight.js > gear-lib > less > request > form-data > mime:
|
||||
patched: '2019-02-25T06:48:21.845Z'
|
||||
'npm:minimatch:20160620':
|
||||
- node-factorio-api > documentation > highlight.js > gear > liftoff > findup-sync > glob > minimatch:
|
||||
patched: '2019-02-25T06:48:21.845Z'
|
||||
- node-factorio-api > documentation > highlight.js > gear-lib > gear > liftoff > findup-sync > glob > minimatch:
|
||||
patched: '2019-02-25T06:48:21.845Z'
|
||||
- node-factorio-api > documentation > highlight.js > gear-lib > glob > minimatch:
|
||||
patched: '2019-02-25T06:48:21.845Z'
|
||||
- node-factorio-api > documentation > highlight.js > gear-lib > jslint > glob > minimatch:
|
||||
patched: '2019-02-25T06:48:21.845Z'
|
||||
- node-factorio-api > documentation > highlight.js > gear-lib > jshint > cli > glob > minimatch:
|
||||
patched: '2019-02-25T06:48:21.845Z'
|
||||
- node-factorio-api > documentation > highlight.js > gear-lib > jshint > minimatch:
|
||||
patched: '2019-02-25T06:48:21.845Z'
|
||||
'npm:request:20160119':
|
||||
- node-factorio-api > documentation > highlight.js > gear-lib > less > request:
|
||||
patched: '2019-02-25T06:48:21.845Z'
|
||||
'npm:tunnel-agent:20170305':
|
||||
- node-factorio-api > documentation > highlight.js > gear-lib > less > request > tunnel-agent:
|
||||
patched: '2019-02-25T06:48:21.845Z'
|
||||
'npm:uglify-js:20151024':
|
||||
- node-factorio-api > documentation > highlight.js > gear-lib > handlebars > uglify-js:
|
||||
patched: '2019-02-25T06:48:21.845Z'
|
||||
- node-factorio-api > documentation > highlight.js > gear-lib > uglify-js:
|
||||
patched: '2019-02-25T06:48:21.845Z'
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"CurrentProjectSetting": null
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"ExpandedNodes": [
|
||||
"",
|
||||
"\\sharedPlugins"
|
||||
],
|
||||
"SelectedNode": "\\.gitignore",
|
||||
"PreviewInSolutionExplorer": false
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
Binary file not shown.
@@ -1,5 +1,13 @@
|
||||
const needle = require("needle");
|
||||
const config = require("./config.json");
|
||||
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 masterIP = `${config.masterIP}:${config.masterPort}`;
|
||||
const token = config.masterAuthToken
|
||||
@@ -30,4 +38,4 @@ let killBiters = `/silent-command local surface=game.surfaces[1] for key, entity
|
||||
// console.log(resp.body)
|
||||
// });
|
||||
// }, 1000*60*5);
|
||||
})();
|
||||
})();
|
||||
@@ -0,0 +1,19 @@
|
||||
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)
|
||||
});
|
||||
+5
-7
@@ -2,22 +2,20 @@ 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
|
||||
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
|
||||
RUN git clone -b master https://github.com/Danielv123/factorioClusterio.git && 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
|
||||
|
||||
WORKDIR /factorioClusterio
|
||||
WORKDIR factorioClusterio
|
||||
RUN mkdir instances sharedMods
|
||||
RUN cp config.json.dist config.json
|
||||
|
||||
RUN node client.js download
|
||||
|
||||
LABEL maintainer "danielv@live.no"
|
||||
LABEL maintainer "Sir3lit@gmail.com"
|
||||
|
||||
EXPOSE 8080 34167
|
||||
VOLUME /factorioClusterio/instances
|
||||
VOLUME /factorioClusterio/sharedMods
|
||||
VOLUME /factorioClusterio/sharedPlugins
|
||||
|
||||
CMD RCONPORT="$RCONPORT" FACTORIOPORT="34167" MODE="$MODE" node $MODE\.js start $INSTANCE
|
||||
CMD RCONPORT="$RCONPORT" FACTORIOPORT="$FACTORIOPORT" MODE="$MODE" node $MODE\.js start $INSTANCE
|
||||
@@ -78,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, independant on what mods are used. Yes, modded items are fully supported.
|
||||
This process works the same for both items and liquids, independent 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.
|
||||
|
||||
@@ -226,6 +226,10 @@ 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:
|
||||
|
||||
sudo setcap 'cap_net_bind_service=+ep' $(readlink -f $(which node))
|
||||
|
||||
### InitializationError: First argument must be an abstract-leveldown compliant store
|
||||
|
||||
do:
|
||||
|
||||
Binary file not shown.
@@ -317,6 +317,7 @@ 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);
|
||||
@@ -364,12 +365,14 @@ write-data=${ path.resolve(config.instanceDirectory, instance) }\r\n
|
||||
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();
|
||||
let client = new Rcon({
|
||||
packetResponseTimeout: 200000,
|
||||
maxPending: 5
|
||||
});
|
||||
client.connect({
|
||||
host: 'localhost',
|
||||
port: Number(process.env.RCONPORT) || instconf.clientPort,
|
||||
password: instconf.clientPassword,
|
||||
timeout: 5000
|
||||
password: instconf.clientPassword
|
||||
});
|
||||
client.onDidAuthenticate(() => {
|
||||
console.log('Clusterio | RCON Authenticated!');
|
||||
@@ -493,8 +496,10 @@ 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();
|
||||
|
||||
client = new Rcon({
|
||||
packetResponseTimeout: 200000, // 200s, should allow for commands up to 1250kB in length
|
||||
maxPending: 5
|
||||
});
|
||||
// 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() {
|
||||
@@ -505,7 +510,6 @@ 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(){
|
||||
@@ -670,6 +674,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, "");
|
||||
|
||||
@@ -47,7 +47,9 @@
|
||||
"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,
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"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
|
||||
@@ -0,0 +1,112 @@
|
||||
"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
|
||||
@@ -0,0 +1,8 @@
|
||||
"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
|
||||
@@ -0,0 +1,177 @@
|
||||
"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
|
||||
@@ -0,0 +1,10 @@
|
||||
"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
|
||||
@@ -0,0 +1,3 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
//# sourceMappingURL=IApiSlaves.js.map
|
||||
@@ -0,0 +1,3 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
//# sourceMappingURL=IClusterioMasterProxy.js.map
|
||||
@@ -0,0 +1,27 @@
|
||||
"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
|
||||
@@ -0,0 +1,24 @@
|
||||
"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
|
||||
@@ -0,0 +1,61 @@
|
||||
"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
|
||||
@@ -0,0 +1,29 @@
|
||||
"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
|
||||
@@ -0,0 +1,57 @@
|
||||
"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
|
||||
@@ -0,0 +1,16 @@
|
||||
"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
|
||||
@@ -0,0 +1,8 @@
|
||||
"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
|
||||
@@ -0,0 +1,64 @@
|
||||
"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
|
||||
@@ -0,0 +1,10 @@
|
||||
"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
|
||||
@@ -0,0 +1,10 @@
|
||||
"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
|
||||
@@ -0,0 +1,13 @@
|
||||
"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
|
||||
@@ -0,0 +1,119 @@
|
||||
//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,15 +0,0 @@
|
||||
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
|
||||
}
|
||||
Vendored
-6
@@ -1,6 +0,0 @@
|
||||
declare module "getFactorioLocale" {
|
||||
interface callback {
|
||||
(error: Error, factorioLocale?: any): void
|
||||
}
|
||||
export function asObject(factorioDirectory: string, languageCode: string, callback: callback): void
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
const fs = require("fs-extra");
|
||||
const needle = require("needle");
|
||||
@@ -36,53 +36,97 @@ require 'hotpatch.gui'
|
||||
-- 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', '1.0.0', [===[
|
||||
local util = require("util")
|
||||
local silo_script = require("silo-script")
|
||||
local version = 1
|
||||
|
||||
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
|
||||
|
||||
script.on_event(defines.events.on_player_created, function(event)
|
||||
local player = game.players[event.player_index]
|
||||
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"})
|
||||
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
|
||||
end
|
||||
silo_script.on_player_created(event)
|
||||
|
||||
silo_script.on_event(event)
|
||||
end)
|
||||
|
||||
script.on_event(defines.events.on_player_respawned, function(event)
|
||||
local player = game.players[event.player_index]
|
||||
player.insert{name="pistol", count=1}
|
||||
player.insert{name="firearm-magazine", count=10}
|
||||
end)
|
||||
|
||||
script.on_event(defines.events.on_gui_click, function(event)
|
||||
silo_script.on_gui_click(event)
|
||||
end)
|
||||
|
||||
script.on_init(function()
|
||||
global.version = version
|
||||
silo_script.on_init()
|
||||
end)
|
||||
|
||||
script.on_event(defines.events.on_rocket_launched, function(event)
|
||||
silo_script.on_rocket_launched(event)
|
||||
util.insert_safe(player, global.respawn_items)
|
||||
silo_script.on_event(event)
|
||||
end)
|
||||
|
||||
script.on_configuration_changed(function(event)
|
||||
if global.version ~= version then
|
||||
global.version = version
|
||||
end
|
||||
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()
|
||||
end)
|
||||
|
||||
script.on_init(function()
|
||||
global.created_items = created_items()
|
||||
global.respawn_items = respawn_items()
|
||||
silo_script.on_init()
|
||||
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
|
||||
@@ -98,7 +142,7 @@ files['testfolder.test'] = [===[
|
||||
]===]
|
||||
|
||||
files['test'] = [===[
|
||||
require 'testfolder.test'
|
||||
require 'testfolder.test'
|
||||
]===]
|
||||
|
||||
hotpatch_tools.static_mod('require-test', '1.0.0', [===[
|
||||
|
||||
@@ -61,11 +61,11 @@ sub_commands = {
|
||||
if (not param) or (param == '') then
|
||||
caller.print('Usage: /hotpatch [command] [parameters]')
|
||||
caller.print('Available commands:')
|
||||
for k, v in pairs(sub_commands) do caller.print(k) end
|
||||
for k, _ 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
|
||||
@@ -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)
|
||||
|
||||
@@ -89,26 +89,26 @@ on_gui_click_handlers = {
|
||||
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.style.visible = true
|
||||
menu.visible = true
|
||||
return
|
||||
end
|
||||
menu.style.visible = not menu.style.visible
|
||||
menu.visible = not menu.visible
|
||||
end,
|
||||
['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.style.visible = not menu.style.visible
|
||||
|
||||
menu.visible = not menu.visible
|
||||
|
||||
on_gui_click_handlers[e.element.name](e)
|
||||
end,
|
||||
['hotpatch-menu.IDE'] = function(e)
|
||||
@@ -116,7 +116,7 @@ on_gui_click_handlers = {
|
||||
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}
|
||||
@@ -132,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_spacing = 0
|
||||
files.style.cell_padding = 0
|
||||
files.style.top_padding = 0
|
||||
files.style.bottom_padding = 0
|
||||
code.style.width = 600
|
||||
code.style.height = 600
|
||||
IDE.style.visible = false
|
||||
IDE.visible = false
|
||||
end
|
||||
|
||||
|
||||
local IDE_dropdown = IDE['hotpatch-IDE-top']['hotpatch-IDE-mod-selector']
|
||||
IDE_dropdown.clear_items()
|
||||
for k, v in ipairs(installed_mods) do
|
||||
for _, v in ipairs(installed_mods) do
|
||||
IDE_dropdown.add_item(v.name)
|
||||
end
|
||||
|
||||
IDE.style.visible = not IDE.style.visible
|
||||
if IDE.style.visible then
|
||||
|
||||
IDE.visible = not IDE.visible
|
||||
if IDE.visible then
|
||||
player.opened = IDE
|
||||
else
|
||||
player.opened = nil
|
||||
@@ -158,15 +158,15 @@ on_gui_click_handlers = {
|
||||
local top = mod_gui.get_button_flow(player)
|
||||
local left = mod_gui.get_frame_flow(player)
|
||||
local center = player.gui.center
|
||||
|
||||
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'}
|
||||
|
||||
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'}
|
||||
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 = console.add{type = 'flow', name = 'hotpatch-console-bottom', direction = 'vertical'}
|
||||
local bottom_flow = hotpatch_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'}
|
||||
@@ -174,53 +174,54 @@ on_gui_click_handlers = {
|
||||
output.style.width = 800
|
||||
output.style.height = 600
|
||||
input.style.width = 600
|
||||
hotpatch_console.visible = false
|
||||
end
|
||||
|
||||
local console_dropdown = console['hotpatch-console-top']['hotpatch-console-mod-selector']
|
||||
|
||||
local console_dropdown = hotpatch_console['hotpatch-console-top']['hotpatch-console-mod-selector']
|
||||
console_dropdown.clear_items()
|
||||
for k, v in ipairs(loaded_mods) do
|
||||
for _, v in ipairs(loaded_mods) do
|
||||
console_dropdown.add_item(v.name)
|
||||
end
|
||||
|
||||
console.style.visible = not console.style.visible
|
||||
if console.style.visible then
|
||||
player.opened = console
|
||||
|
||||
hotpatch_console.visible = not hotpatch_console.visible
|
||||
if hotpatch_console.visible then
|
||||
player.opened = hotpatch_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 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']
|
||||
|
||||
|
||||
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']
|
||||
|
||||
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(...)
|
||||
@@ -230,21 +231,22 @@ 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
|
||||
local success, err = pcall(code)
|
||||
if code then
|
||||
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,
|
||||
@@ -253,10 +255,10 @@ on_gui_click_handlers = {
|
||||
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 k, v in pairs(center['hotpatch-IDE']['hotpatch-IDE-table']['hotpatch-IDE-files']['hotpatch-IDE-files-table'].children) do
|
||||
for _, 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
|
||||
@@ -297,7 +299,7 @@ on_gui_selection_state_changed_handlers = {
|
||||
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
|
||||
@@ -306,8 +308,8 @@ 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 console = center['hotpatch-console']
|
||||
local output = console['hotpatch-console-bottom']['hotpatch-console-output']
|
||||
local hotpatch_console = center['hotpatch-console']
|
||||
local output = hotpatch_console['hotpatch-console-bottom']['hotpatch-console-output']
|
||||
output.text = ''
|
||||
end
|
||||
end
|
||||
@@ -317,9 +319,9 @@ on_gui_selection_state_changed_handlers = {
|
||||
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)
|
||||
@@ -332,7 +334,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, v in pairs(mod.files) do
|
||||
for k, _ 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
|
||||
@@ -356,20 +358,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.style.visible then
|
||||
if element.visible then
|
||||
player.play_sound{path = "utility/gui_click"}
|
||||
end
|
||||
element.style.visible = false
|
||||
element.visible = false
|
||||
elseif element.name == 'hotpatch-console' then
|
||||
if element.style.visible then
|
||||
if element.visible then
|
||||
player.play_sound{path = "utility/gui_click"}
|
||||
end
|
||||
element.style.visible = false
|
||||
element.visible = false
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
]===])
|
||||
--]===])
|
||||
|
||||
return true
|
||||
@@ -67,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
|
||||
@@ -101,6 +101,8 @@ 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
|
||||
@@ -222,12 +224,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
|
||||
@@ -261,7 +263,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)
|
||||
@@ -285,12 +287,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
|
||||
@@ -324,7 +326,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
|
||||
@@ -363,7 +365,7 @@ debug_log({'hotpatch-info.logging-enabled'})
|
||||
|
||||
-- track _ENV accesses
|
||||
setmetatable(_ENV, {
|
||||
__index = function(t, k)
|
||||
__index = function(_, k)
|
||||
debug_log({'hotpatch-trace.nil-var-access', k}, nil, 3)
|
||||
return nil
|
||||
end,
|
||||
@@ -382,7 +384,6 @@ debug_log({'hotpatch-info.loading-libs'})
|
||||
local libraries = {
|
||||
'camera',
|
||||
'flying_tags',
|
||||
'inspect',
|
||||
'math3d',
|
||||
'mod-gui',
|
||||
'noise',
|
||||
@@ -429,14 +430,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
|
||||
|
||||
@@ -447,7 +448,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('/', '.')
|
||||
@@ -475,7 +476,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
|
||||
@@ -483,7 +484,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)
|
||||
@@ -501,7 +502,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,
|
||||
@@ -516,7 +517,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 = {}
|
||||
@@ -541,7 +542,7 @@ load_mod = function(installed_index)
|
||||
mod_script.on_nth_tick = function(tick, f)
|
||||
if tick then
|
||||
if type(tick) == 'table' then
|
||||
for k, v in pairs(tick) do
|
||||
for _, v in pairs(tick) do
|
||||
mod_script.on_nth_tick(v, f)
|
||||
end
|
||||
return
|
||||
@@ -551,12 +552,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, event)
|
||||
register_on_tick(mod_name)
|
||||
end
|
||||
else
|
||||
mod_obj.on_event[event] = f
|
||||
@@ -568,7 +569,7 @@ load_mod = function(installed_index)
|
||||
mod_script.on_nth_tick = function(tick, f)
|
||||
if tick then
|
||||
if type(tick) == 'table' then
|
||||
for k, v in pairs(tick) do
|
||||
for _, v in pairs(tick) do
|
||||
mod_script.on_nth_tick(v, f)
|
||||
end
|
||||
return
|
||||
@@ -578,11 +579,11 @@ load_mod = function(installed_index)
|
||||
register_nth_tick(mod_name, tick)
|
||||
end
|
||||
else
|
||||
local on_nth_tick = mod_obj.on_nth_tick
|
||||
local mod_on_nth_tick = mod_obj.on_nth_tick
|
||||
mod_obj.on_nth_tick = {}
|
||||
if mod_obj.running then
|
||||
for k, v in pairs(on_nth_tick) do
|
||||
register_nth_tick(mod_name, tick)
|
||||
for _, v in pairs(mod_on_nth_tick) do
|
||||
register_nth_tick(mod_name, v)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -604,7 +605,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
|
||||
@@ -621,20 +622,20 @@ load_mod = function(installed_index)
|
||||
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.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
|
||||
@@ -650,7 +651,7 @@ load_mod = function(installed_index)
|
||||
return remote.call(...)
|
||||
end,
|
||||
interfaces = setmetatable({}, {
|
||||
__index = function(t, k) return remote.interfaces[k] end,
|
||||
__index = function(_, 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
|
||||
})
|
||||
}
|
||||
@@ -666,16 +667,16 @@ load_mod = function(installed_index)
|
||||
return commands.remove_command(name)
|
||||
end,
|
||||
commands = setmetatable({}, {
|
||||
__index = function(t, k) return commands.commands[k] end,
|
||||
__index = function(_, 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(t, k) return commands.game_commands[k] end,
|
||||
__index = function(_, 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('/', '.')
|
||||
@@ -710,7 +711,7 @@ load_mod = function(installed_index)
|
||||
error(err)
|
||||
end
|
||||
end
|
||||
local file = global.mods[installed_index].files[alt_path]
|
||||
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)
|
||||
@@ -724,31 +725,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(path .. ' not found', nil, 3)
|
||||
if not lib then
|
||||
debug_log({'hotpatch-error.path-not-found', path}, mod_name, 3)
|
||||
error(path .. ' not found')
|
||||
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(t, k) return game[k] end,
|
||||
__index = function(_, 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(t, k)
|
||||
mt.__index = function(_, k)
|
||||
debug_log({'hotpatch-trace.nil-var-access', k}, nil, 3)
|
||||
return nil
|
||||
end
|
||||
@@ -762,7 +763,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)
|
||||
@@ -816,12 +817,12 @@ reset_mod = function(loaded_index)
|
||||
end
|
||||
|
||||
reset_mod_events = function(loaded_index)
|
||||
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]
|
||||
--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
|
||||
loaded_mod.on_event = {}
|
||||
loaded_mod.on_nth_tick = {}
|
||||
loaded_mod.on_init = nil
|
||||
@@ -842,7 +843,7 @@ run_mod = function(loaded_index)
|
||||
--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)
|
||||
@@ -851,22 +852,22 @@ run_mod = function(loaded_index)
|
||||
if game and game.player then
|
||||
game.player.print(result)
|
||||
end
|
||||
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
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
|
||||
for k,_ in pairs(mod.env.global) do
|
||||
mod.env.global[k] = nil
|
||||
end
|
||||
for k,v in pairs(old_global) do
|
||||
@@ -875,16 +876,16 @@ run_mod = function(loaded_index)
|
||||
--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, event)
|
||||
register_on_tick(mod_name)
|
||||
else
|
||||
mod.on_event[event] = f
|
||||
register_event(mod_name, event)
|
||||
@@ -893,18 +894,18 @@ run_mod = function(loaded_index)
|
||||
mod.env.script.on_nth_tick = function(tick, f)
|
||||
if tick then
|
||||
if type(tick) == 'table' then
|
||||
for k, v in pairs(tick) do
|
||||
mod_script.on_nth_tick(v, f)
|
||||
for _, v in pairs(tick) do
|
||||
mod.env.script.on_nth_tick(v, f)
|
||||
end
|
||||
return
|
||||
end
|
||||
mod.on_nth_tick[tick] = f
|
||||
register_nth_tick(mod_name, tick)
|
||||
else
|
||||
local on_nth_tick = mod.on_nth_tick
|
||||
local mod_on_nth_tick = mod.on_nth_tick
|
||||
mod.on_nth_tick = {}
|
||||
for k, v in pairs(on_nth_tick) do
|
||||
register_nth_tick(mod_name, tick)
|
||||
for _, v in pairs(mod_on_nth_tick) do
|
||||
register_nth_tick(mod_name, v)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -924,7 +925,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
|
||||
@@ -940,7 +941,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
|
||||
@@ -954,7 +955,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
|
||||
@@ -962,7 +963,7 @@ else
|
||||
end
|
||||
end
|
||||
|
||||
local register_all_events = function()
|
||||
register_all_events = function()
|
||||
--unregister all events
|
||||
script.on_event(defines.events, nil)
|
||||
script.on_nth_tick(nil, nil)
|
||||
@@ -981,12 +982,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,v in pairs(mod.on_event) do
|
||||
for k,_ 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,v in pairs(mod.on_nth_tick) do
|
||||
for k,_ 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
|
||||
@@ -994,14 +995,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('on_init failed for ' .. mod.name)
|
||||
debug_log(result)
|
||||
debug_log({'hotpatch-error.on-init-failed'}, mod_name)
|
||||
debug_log(result, mod_name)
|
||||
return false
|
||||
end
|
||||
end
|
||||
@@ -1013,15 +1014,15 @@ 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
|
||||
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('on_load failed for ' .. mod.name)
|
||||
debug_log(result)
|
||||
debug_log({'hotpatch-error.on-load-failed'}, mod_name)
|
||||
debug_log(result, mod_name)
|
||||
return false
|
||||
end
|
||||
end
|
||||
@@ -1033,14 +1034,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('on_configuration_changed failed for ' .. mod.name)
|
||||
debug_log(result)
|
||||
debug_log({'hotpatch-error.on-configuration-changed-failed'}, mod_name)
|
||||
debug_log(result, mod_name)
|
||||
return false
|
||||
end
|
||||
end
|
||||
@@ -1058,9 +1059,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)
|
||||
@@ -1078,9 +1079,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)
|
||||
@@ -1126,7 +1127,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
|
||||
@@ -1137,7 +1138,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)
|
||||
@@ -1153,10 +1154,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 = {}
|
||||
@@ -1174,19 +1175,19 @@ 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 = {}
|
||||
for i = 1, #loaded_mods do
|
||||
@@ -1198,12 +1199,12 @@ on_load = function()
|
||||
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
|
||||
|
||||
|
||||
debug_log({'hotpatch-info.complete', {'hotpatch-info.loading-installed-mods'}})
|
||||
debug_log({'hotpatch-info.complete', {'hotpatch-info.on-load'}})
|
||||
end
|
||||
@@ -1212,11 +1213,11 @@ on_configuration_changed = function(config)
|
||||
debug_log({'hotpatch-info.on-configuration-changed'})
|
||||
local failed_mods = {}
|
||||
for i = 1, #loaded_mods do
|
||||
if not mod_on_configuration_changed(i) then
|
||||
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])
|
||||
end
|
||||
@@ -1231,7 +1232,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
|
||||
@@ -1244,7 +1245,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
|
||||
@@ -1286,31 +1287,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(t, k)
|
||||
__index = function(_, k)
|
||||
return global.mods[k]
|
||||
end,
|
||||
__newindex = function(t, k, v)
|
||||
__newindex = function(_, __, ___)
|
||||
-- do nothing
|
||||
end,
|
||||
__len = function(t)
|
||||
__len = function(_)
|
||||
return #global.mods
|
||||
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,
|
||||
__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,
|
||||
__metatable = false,
|
||||
})
|
||||
},{
|
||||
__index = function(t, k)
|
||||
__index = function(_, k)
|
||||
debug_log({'hotpatch-error.invalid-API-access', k}, nil, 3)
|
||||
end,
|
||||
__newindex = function(t, k, v)
|
||||
__newindex = function(_, __, ___)
|
||||
-- do nothing
|
||||
end,
|
||||
-- Don't let mods muck around
|
||||
@@ -1322,11 +1323,12 @@ 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(_ENV.debug_settings.level) or debug_levels[_ENV.debug_settings.level]
|
||||
debug_level = tonumber(level) or debug_levels[level]
|
||||
end,
|
||||
},{
|
||||
__index = mod_tools_internal,
|
||||
__newindex = function(t, k, v)
|
||||
__newindex = function(_, __, ___)
|
||||
-- do nothing, read-only table
|
||||
end,
|
||||
-- Don't let mods muck around
|
||||
|
||||
@@ -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,v in pairs(mod_files) do
|
||||
for k,_ 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('WARNING: mod already exists: ' .. mod_name .. ' ' .. old_version)
|
||||
debug_log('WARNING: reinstalling mod in-place: ' .. mod_name .. ' ' .. mod_version)
|
||||
debug_log({'hotpatch-warning.already-exists', mod_name, old_version})
|
||||
debug_log({'hotpatch-warning.reinstalling', mod_name, old_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,6 +131,8 @@ 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)
|
||||
@@ -139,13 +141,13 @@ remote_interface['run'] = function(mod_name)
|
||||
loaded_index = find_loaded_mod(mod_name)
|
||||
else
|
||||
if loaded_index then
|
||||
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)
|
||||
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})
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
if loaded_index then
|
||||
if not run_mod(loaded_index) then
|
||||
caller.print('execution failed for mod ' .. mod_name)
|
||||
@@ -164,7 +166,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
|
||||
@@ -178,7 +180,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,v in pairs(mod_files) do
|
||||
for k,_ in pairs(mod_files) do
|
||||
if k == 'control' then
|
||||
caller.print('ERROR: control.lua loaded twice for mod ' .. mod_name)
|
||||
return
|
||||
@@ -186,6 +188,9 @@ 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
|
||||
@@ -245,16 +250,40 @@ 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)
|
||||
mod_on_configuration_changed(mod_name, {mod_changes = {mod_name={new_version=mod_version}}})
|
||||
--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}}})
|
||||
else
|
||||
caller.print('You must be an admin to run this command.')
|
||||
end
|
||||
@@ -274,13 +303,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
|
||||
]===]
|
||||
|
||||
|
||||
@@ -65,13 +65,17 @@ 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-loaded=mod already loaded
|
||||
reinitializing=reinitializing...
|
||||
already-exists=mod already exists
|
||||
reinstalling=reinstalling mod in-place
|
||||
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__
|
||||
|
||||
[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
|
||||
Vendored
-16
@@ -1,16 +0,0 @@
|
||||
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,
|
||||
}
|
||||
+235
-192
@@ -1,6 +1,3 @@
|
||||
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
|
||||
@@ -16,9 +13,7 @@ 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
|
||||
@@ -29,7 +24,7 @@ process.title = "clusterioMaster";
|
||||
process.on('unhandledRejection', r => console.log(r));
|
||||
|
||||
// configgy stuff
|
||||
const debug = false;
|
||||
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
|
||||
@@ -68,7 +63,7 @@ const fileOps = require("_app/fileOps");
|
||||
const authenticate = require("./lib/authenticate")(config);
|
||||
|
||||
/** Sync */
|
||||
function randomStringAsBase64Url(size: Number) {
|
||||
function randomStringAsBase64Url(size) {
|
||||
return base64url(crypto.randomBytes(size));
|
||||
}
|
||||
if (!fs.existsSync("secret-api-token.txt")) {
|
||||
@@ -110,11 +105,11 @@ app.set('view engine', 'html');
|
||||
app.engine('html', ejs.renderFile);
|
||||
|
||||
// give ejs access to some interesting information
|
||||
app.use(function(req: Request, res: any, next: any){
|
||||
app.use(function(req, res, next){
|
||||
res.locals.res = res;
|
||||
res.locals.req = req;
|
||||
res.locals.masterPlugins = masterPlugins;
|
||||
res.locals.slaves = db.slaves;
|
||||
res.locals.slaves = slaves;
|
||||
res.locals.moment = moment;
|
||||
next();
|
||||
});
|
||||
@@ -136,7 +131,7 @@ if(!config.disablePrometheusPushgateway){
|
||||
setInterval(() => {
|
||||
registerMoreMetrics();
|
||||
pushgateway.push({ jobName: 'clusterio', groupings: {instance: config.publicIP + ":" + config.masterPort, owner: config.username}}, function(err, resp, body) {})
|
||||
}, 5000)
|
||||
}, 15000)
|
||||
}
|
||||
|
||||
// collect express request durations ms
|
||||
@@ -210,15 +205,15 @@ app.get('/metrics', (req, res) => {
|
||||
});
|
||||
|
||||
function registerMoreMetrics(){
|
||||
for(let instanceID in db.slaves){
|
||||
for(let instanceID in slaves){
|
||||
// playercount
|
||||
try{
|
||||
prometheusPlayerCountGauge.labels(instanceID, db.slaves[instanceID].instanceName).set(Number(db.slaves[instanceID].playerCount) || 0);
|
||||
prometheusPlayerCountGauge.labels(instanceID, slaves[instanceID].instanceName).set(Number(slaves[instanceID].playerCount) || 0);
|
||||
}catch(e){}
|
||||
// UPS
|
||||
try{
|
||||
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);
|
||||
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);
|
||||
}catch(e){}
|
||||
}
|
||||
// inventory
|
||||
@@ -229,15 +224,58 @@ function registerMoreMetrics(){
|
||||
}
|
||||
// Slave count
|
||||
let numberOfActiveSlaves = 0;
|
||||
for(let instance in db.slaves){
|
||||
if(Date.now() - Number(db.slaves[instance].time) < 1000 * 30) numberOfActiveSlaves++;
|
||||
for(let instance in slaves){
|
||||
if(Date.now() - Number(slaves[instance].time) < 1000 * 30) numberOfActiveSlaves++;
|
||||
}
|
||||
prometheusConnectedInstancesCounter.set(numberOfActiveSlaves);
|
||||
}
|
||||
|
||||
// set up database
|
||||
const db: db = getDatabase(config);
|
||||
const db = {};
|
||||
|
||||
(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
|
||||
@@ -245,29 +283,35 @@ process.on('SIGHUP', shutdown); // terminal closed
|
||||
async function shutdown() {
|
||||
console.log('Ctrl-C...');
|
||||
let exitStartTime = Date.now();
|
||||
// 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");
|
||||
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)
|
||||
}
|
||||
console.log("Clusterio cleanly exited in "+(Date.now()-exitStartTime)+"ms");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -276,7 +320,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}
|
||||
@@ -286,12 +330,12 @@ app.post("/api/getID", authenticate.middleware, function(req, res) {
|
||||
if(debug){
|
||||
console.log(req.body)
|
||||
}
|
||||
if(!db.slaves[req.body.unique]){
|
||||
db.slaves[req.body.unique] = {};
|
||||
if(!slaves[req.body.unique]){
|
||||
slaves[req.body.unique] = {};
|
||||
}
|
||||
for(let k in req.body){
|
||||
for(k in req.body){
|
||||
if(k != "meta" && req.body.hasOwnProperty(k)){
|
||||
db.slaves[req.body.unique][k] = req.body[k];
|
||||
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);
|
||||
@@ -308,16 +352,18 @@ app.post("/api/editSlaveMeta", authenticate.middleware, function(req,res) {
|
||||
// request.body should be an object
|
||||
// {instanceID, pass, meta:{x,y,z}}
|
||||
|
||||
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 = {};
|
||||
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 = {};
|
||||
}
|
||||
db.slaves[req.body.instanceID].meta = deepmerge(db.slaves[req.body.instanceID].meta, req.body.meta, {clone:true});
|
||||
slaves[req.body.instanceID].meta = deepmerge(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")
|
||||
}
|
||||
@@ -333,12 +379,20 @@ 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 && req.body.password){
|
||||
console.log("returning meta for ", req.body.instanceID);
|
||||
res.send(JSON.stringify(db.slaves[req.body.instanceID].meta))
|
||||
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"}')
|
||||
}
|
||||
} else {
|
||||
res.status(400);
|
||||
res.send('{"INVALID REQUEST":1}');
|
||||
res.status(400);
|
||||
res.send('{"INVALID_REQUEST":1}');
|
||||
}
|
||||
});
|
||||
// mod management
|
||||
@@ -349,11 +403,11 @@ POST Check if a mod has been uploaded to the master before. Only checks against
|
||||
@instance
|
||||
@alias /api/checkMod
|
||||
*/
|
||||
app.post("/api/checkMod", function(req,res) {
|
||||
app.post("/api/checkMod", authenticate.middleware, function(req,res) {
|
||||
endpointHitCounter.labels(req.route.path).inc();
|
||||
let files: string[] = fs.readdirSync(masterModFolder);
|
||||
let files = fs.readdirSync(masterModFolder);
|
||||
let found = false;
|
||||
files.forEach((file) => {
|
||||
files.forEach(file => {
|
||||
if(file == req.body.modName) {
|
||||
found = true;
|
||||
}
|
||||
@@ -376,7 +430,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: Error) {
|
||||
req.files.file.mv(path.resolve(config.databaseDirectory, "masterMods", req.files.file.name), function(err) {
|
||||
if (err) {
|
||||
res.status(500).send(err);
|
||||
} else {
|
||||
@@ -390,37 +444,59 @@ app.post("/api/uploadMod", authenticate.middleware, function(req,res) {
|
||||
}
|
||||
});
|
||||
/**
|
||||
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");
|
||||
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();
|
||||
let slaveCache = getSlaves();
|
||||
for(key in slaveCache.cache) {
|
||||
slaveCache.cache[key].rconPassword = "hidden";
|
||||
}
|
||||
|
||||
res.send(slaveCache.cache);
|
||||
});
|
||||
var recievedItemStatisticsBySlaveID: any = {};
|
||||
var sentItemStatisticsBySlaveID: any = {};
|
||||
/**
|
||||
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};
|
||||
}
|
||||
res.send(slaveCache.cache);
|
||||
});
|
||||
/*
|
||||
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.
|
||||
@@ -437,7 +513,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: item;
|
||||
let x;
|
||||
try {
|
||||
x = JSON.parse(req.body);
|
||||
} catch (e) {
|
||||
@@ -448,8 +524,8 @@ app.post("/api/place", authenticate.middleware, function(req, res) {
|
||||
}
|
||||
if(!x.instanceID) {
|
||||
x.instanceID = "unknown";
|
||||
Object.keys(db.slaves).forEach(instanceID => {
|
||||
if(db.slaves[instanceID].instanceName == req.body.instanceName) {
|
||||
Object.keys(slaves).forEach(instanceID => {
|
||||
if(slaves[instanceID].instanceName == req.body.instanceName) {
|
||||
x.instanceID = instanceID;
|
||||
}
|
||||
});
|
||||
@@ -490,6 +566,13 @@ app.post("/api/place", authenticate.middleware, function(req, res) {
|
||||
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.
|
||||
|
||||
@@ -501,11 +584,8 @@ 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
|
||||
@@ -516,10 +596,10 @@ app.post("/api/remove", authenticate.middleware, function(req, res) {
|
||||
if(!object.instanceName) {
|
||||
object.instanceName = "unknown";
|
||||
}
|
||||
if(db.slaves[object.instanceID]) object.instanceName = db.slaves[object.instanceID].instanceName;
|
||||
if(slaves[object.instanceID]) object.instanceName = slaves[object.instanceID].instanceName;
|
||||
let item = db.items[object.name]
|
||||
// console.dir(doc);
|
||||
if(!item
|
||||
if((item === undefined)
|
||||
||((
|
||||
config.disableImportsOfEverythingExceptElectricity === true || config.disableImportsOfEverythingExceptElectricity === "true" )
|
||||
&& object.name != "electricity")
|
||||
@@ -529,6 +609,7 @@ 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});
|
||||
@@ -549,45 +630,27 @@ 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 {
|
||||
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+")");
|
||||
}
|
||||
// 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,
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
@@ -634,18 +697,18 @@ app.get("/api/inventoryAsObject", function(req, res) {
|
||||
|
||||
@memberof clusterioMaster
|
||||
@instance
|
||||
@alias api/modmeta
|
||||
@alias api/modData
|
||||
@returns {object} JSON
|
||||
*/
|
||||
|
||||
// wrap a request in an promise
|
||||
async function downloadPage(url: string): Promise<object> {
|
||||
return new Promise((resolve, reject) => {
|
||||
request(url, (_error: any, _response: any, body: object) => {
|
||||
resolve(body);
|
||||
});
|
||||
});
|
||||
}
|
||||
async function downloadPage(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
request(url, (error, response, body) => {
|
||||
resolve(body);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
app.get("/api/modmeta", async function(req, res) {
|
||||
endpointHitCounter.labels(req.route.path).inc();
|
||||
@@ -666,45 +729,38 @@ 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", (req,res) => {
|
||||
app.post("/api/runCommand", authenticate.middleware, function(req,res) {
|
||||
endpointHitCounter.labels(req.route.path).inc();
|
||||
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;
|
||||
// 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: string) => {
|
||||
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 => {
|
||||
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.
|
||||
@@ -716,7 +772,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);
|
||||
});
|
||||
});
|
||||
@@ -725,7 +781,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'));
|
||||
|
||||
var httpsServer = require("https").createServer({
|
||||
httpsServer = require("https").createServer({
|
||||
key: privateKey,
|
||||
cert: certificate
|
||||
}, app).listen(args.sslPort || config.sslPort);
|
||||
@@ -734,11 +790,10 @@ if(args.sslPort || config.sslPort){
|
||||
}
|
||||
}
|
||||
if(args.masterPort || config.masterPort){
|
||||
var server = require("http").Server(app);
|
||||
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 */
|
||||
@@ -747,9 +802,6 @@ 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;
|
||||
@@ -780,10 +832,6 @@ class slaveMapper {
|
||||
}
|
||||
var mapRequesters = {};
|
||||
class mapRequester {
|
||||
requesterID: string
|
||||
socket: any
|
||||
instanceID: string
|
||||
lastBeat: number
|
||||
constructor(requesterID, socket, instanceID){
|
||||
this.requesterID = requesterID;
|
||||
this.socket = socket;
|
||||
@@ -823,13 +871,7 @@ 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;
|
||||
@@ -861,8 +903,8 @@ class wsSlave {
|
||||
prometheusWsUsageCounter.labels('gameChat', this.instanceID).inc();
|
||||
if(typeof data === "object"){
|
||||
data.timestamp = Date.now();
|
||||
if(!wsChatRecievers) wsChatRecievers = [];
|
||||
wsChatRecievers.forEach(socket => {
|
||||
if(!global.wsChatRecievers) global.wsChatRecievers = [];
|
||||
global.wsChatRecievers.forEach(socket => {
|
||||
socket.emit("gameChat", data);
|
||||
});
|
||||
}
|
||||
@@ -872,8 +914,8 @@ class wsSlave {
|
||||
prometheusWsUsageCounter.labels('alert', this.instanceID).inc();
|
||||
if(typeof alert === "object"){
|
||||
alert.timestamp = Date.now();
|
||||
if(!wsAlertRecievers) wsAlertRecievers = [];
|
||||
wsAlertRecievers.forEach(socket => {
|
||||
if(!global.wsAlertRecievers) global.wsAlertRecievers = [];
|
||||
global.wsAlertRecievers.forEach(socket => {
|
||||
socket.emit("alert", alert);
|
||||
});
|
||||
}
|
||||
@@ -928,26 +970,27 @@ io.on('connection', function (socket) {
|
||||
});
|
||||
socket.on("registerChatReciever", function(data){
|
||||
prometheusWsUsageCounter.labels("registerChatReciever", "other").inc();
|
||||
if(!wsChatRecievers) wsChatRecievers = [];
|
||||
wsChatRecievers.push(socket);
|
||||
if(!global.wsChatRecievers) global.wsChatRecievers = [];
|
||||
global.wsChatRecievers.push(socket);
|
||||
});
|
||||
socket.on("registerAlertReciever", function(data){
|
||||
prometheusWsUsageCounter.labels("registerAlertReciever", "other").inc();
|
||||
if(!wsAlertRecievers) wsAlertRecievers = [];
|
||||
wsAlertRecievers.push(socket);
|
||||
if(!global.wsAlertRecievers) global.wsAlertRecievers = [];
|
||||
global.wsAlertRecievers.push(socket);
|
||||
});
|
||||
});
|
||||
|
||||
// handle plugins on the master
|
||||
pluginManagement();
|
||||
var masterPlugins: MasterPlugin[] = [];
|
||||
masterPlugins = [];
|
||||
async function pluginManagement(){
|
||||
let startPluginLoad = Date.now();
|
||||
masterPlugins = await getPlugins();
|
||||
console.log("All plugins loaded in "+(Date.now() - startPluginLoad)+"ms");
|
||||
return;
|
||||
}
|
||||
async function getPlugins(): Promise<MasterPlugin[]>{
|
||||
|
||||
async function getPlugins(){
|
||||
const pluginManager = require("./lib/manager/pluginManager.js")(config);
|
||||
let plugins = [];
|
||||
let pluginsToLoad = await pluginManager.getPlugins();
|
||||
@@ -981,5 +1024,5 @@ async function getPlugins(): Promise<MasterPlugin[]>{
|
||||
return plugins;
|
||||
}
|
||||
|
||||
module.exports = app;
|
||||
})();
|
||||
}
|
||||
@@ -76,6 +76,17 @@ 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(){
|
||||
|
||||
Binary file not shown.
+7
-18
@@ -5,17 +5,15 @@
|
||||
"repository": "https://github.com/Danielv123/factorioClusterio",
|
||||
"main": "master.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"watch-ts": "tsc -w",
|
||||
"watch-node": "nodemon dist/master.js",
|
||||
"watch": "concurrently -k -p \"[{name}]\" -n \"TypeScript, Node\" -c \"yello.bold, cyan.bold\" \"npm run watch-ts\" \"npm run watch-node\"",
|
||||
"test": "./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha -- \"./{{,node_modules/_app/**/},!(node_modules)/**/}*.spec.js\" -R spec || echo .",
|
||||
"start": "echo Run node client.js start [instance] to start slaves && node master.js",
|
||||
"cover": "./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha -- \"./{{,node_modules/_app/**/},!(node_modules)/**/}*.spec.js\" -R spec || echo . && codecov -t f7b38085-36eb-4d72-84e6-64721910171e",
|
||||
"docs": "documentation build index.js --config documentation.yml -f html -o docs",
|
||||
"livedocs": "documentation serve --watch *.js lib/** --config documentation.yml",
|
||||
"install": "node lib/npmPostinstall.js",
|
||||
"bundle-dependencies": "bundle-dependencies"
|
||||
"bundle-dependencies": "bundle-dependencies",
|
||||
"snyk-protect": "snyk protect",
|
||||
"prepublish": "npm run snyk-protect"
|
||||
},
|
||||
"keywords": [
|
||||
"factorio"
|
||||
@@ -23,17 +21,6 @@
|
||||
"author": "Danielv123",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@types/bcrypt": "^3.0.0",
|
||||
"@types/body-parser": "^1.17.0",
|
||||
"@types/deepmerge": "^2.2.0",
|
||||
"@types/ejs": "^2.6.3",
|
||||
"@types/express": "^4.16.1",
|
||||
"@types/fs-extra": "^5.0.5",
|
||||
"@types/mkdirp": "^0.5.2",
|
||||
"@types/moment": "^2.13.0",
|
||||
"@types/needle": "^2.0.4",
|
||||
"@types/request": "^2.48.1",
|
||||
"@types/socket.io": "^2.1.2",
|
||||
"app-root-path": "^2.0.1",
|
||||
"as-table": "^1.0.31",
|
||||
"averaged-timeseries": "^1.4.0",
|
||||
@@ -43,6 +30,7 @@
|
||||
"bcryptjs": "^2.4.3",
|
||||
"body-parser": "^1.18.2",
|
||||
"cache-require-paths": "^0.3.0",
|
||||
"cli": "^1.0.1",
|
||||
"cli-interact": "^0.1.9",
|
||||
"codecov": "^3.0.1",
|
||||
"compression": "^1.7.2",
|
||||
@@ -69,18 +57,19 @@
|
||||
"node-factorio-api": "^0.3.1-alpha",
|
||||
"node-forge": "^0.7.1",
|
||||
"prom-client": "^10.2.3",
|
||||
"rcon-client": "^3.0.2",
|
||||
"rcon-client": "git+https://gitlab.com/Hornwitser/rcon-client.git",
|
||||
"request": "^2.83.0",
|
||||
"right-pad": "^1.0.1",
|
||||
"rmdir-sync": "^1.0.1",
|
||||
"sanitizer": "^0.1.3",
|
||||
"simple-git": "^1.96.0",
|
||||
"single-line-log": "^1.1.2",
|
||||
"snyk": "^1.134.2",
|
||||
"socket.io": "^2.0.4",
|
||||
"socket.io-client": "^2.0.4",
|
||||
"socket.io-prometheus": "^0.2.1",
|
||||
"sync-request": "^4.0.1",
|
||||
"typescript-require": "^0.2.10",
|
||||
"typescript-collections": "^1.3.2",
|
||||
"underscore": "^1.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -11,4 +11,7 @@ module.exports = function(app) {
|
||||
app.get("/nodeDetails", function(req, res) {
|
||||
res.render("nodeDetails");
|
||||
});
|
||||
app.get("/remoteMap", function(req, res) {
|
||||
res.render("remoteMap");
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,183 @@
|
||||
const averagedTimeSeries = require("averaged-timeseries")
|
||||
|
||||
const doleNN = require("./../../lib/dole_nn_base.js")
|
||||
|
||||
const Prometheus = require('prom-client');
|
||||
|
||||
class neuralDole {
|
||||
|
||||
getRequestStats(itemname,samples)
|
||||
{
|
||||
var sum=0;
|
||||
samples=Math.min(samples,(this.stats[itemname] || []).length-1);
|
||||
if (samples<1) return 0.1;
|
||||
for (var i = 1; i <= samples; i++) {
|
||||
sum+=this.stats[itemname][i].req;
|
||||
}
|
||||
if (sum==0) return 0.1;
|
||||
return sum/samples;
|
||||
}
|
||||
|
||||
constructor({
|
||||
items, gaugePrefix
|
||||
}){
|
||||
// Set some storage variables for the dole divider
|
||||
this.prometheusNNDoleGauge = new Prometheus.Gauge({
|
||||
name: gaugePrefix+'nn_dole_gauge',
|
||||
help: 'Current demand being supplied by Neural Network ; 1 means all demand covered, 0.5 means about half of each supply is covered, 0 means no items are given',
|
||||
labelNames: ["itemName"],
|
||||
});
|
||||
this.items = items
|
||||
this.itemsLastTick = JSON.parse(JSON.stringify(items))
|
||||
this.dole = {}
|
||||
this.carry = {}
|
||||
this.lastRequest = {}
|
||||
this.stats=[]
|
||||
this.debt= {}
|
||||
|
||||
setInterval(()=>{
|
||||
for(let name in this.items){
|
||||
// The items object have 2 functions tied to it - addItem and removeItem. A for loop will reveal those properties.
|
||||
if(typeof this.items[name] !== "number") continue
|
||||
|
||||
let count = this.items[name]
|
||||
let magicData = doleNN.Tick(
|
||||
count,
|
||||
this.dole[name],
|
||||
this.itemsLastTick[name],
|
||||
this.getRequestStats(name,5)
|
||||
)
|
||||
this.stats[name]=this.stats[name] || []
|
||||
this.stats[name].unshift({req:0,given:0});//stats[name][0] is the one we currently collect
|
||||
if (this.stats[name].length>10) this.stats[name].pop();//remove if too many samples in stats
|
||||
|
||||
this.dole[name] = magicData[0]
|
||||
// DONE handle magicData[1] for graphing for our users
|
||||
this.prometheusNNDoleGauge.labels(name).set(magicData[1] || 0);
|
||||
}
|
||||
this.itemsLastTick = JSON.parse(JSON.stringify(this.items))
|
||||
}, 1000)
|
||||
}
|
||||
divider({
|
||||
res,
|
||||
object,
|
||||
config,
|
||||
sentItemStatisticsBySlaveID,
|
||||
prometheusImportGauge
|
||||
}){
|
||||
let magicData = doleNN.Dose(
|
||||
object.count, // numReq
|
||||
this.items[object.name],
|
||||
this.itemsLastTick[object.name] || 0,
|
||||
this.dole[object.name],
|
||||
this.carry[object.name+" "+object.instanceID] || 0,
|
||||
this.lastRequest[object.name+"_"+object.instanceID+"_"+object.instanceName] || 0,
|
||||
this.getRequestStats(object.name,5),
|
||||
this.debt[object.name+" "+object.instanceID] || 0
|
||||
)
|
||||
if ((this.stats[object.name] || []).length>0)
|
||||
{
|
||||
this.stats[object.name][0].req+=Number(object.count);
|
||||
this.stats[object.name][0].given+=Number(magicData[0]);
|
||||
}
|
||||
this.lastRequest[object.name+"_"+object.instanceID+"_"+object.instanceName] = object.count
|
||||
//0.Number of items to give in that dose
|
||||
//1.New dole for item X
|
||||
//2.New carry for item X slave Y
|
||||
this.dole[object.name] = magicData[1]
|
||||
this.carry[object.name+" "+object.instanceID] = magicData[2]
|
||||
this.debt[object.name+" "+object.instanceID] = magicData[3]
|
||||
|
||||
// Remove item from DB and send it
|
||||
if(this.items.removeItem({count: magicData[0], name: object.name})){
|
||||
if(config.logItemTransfers){
|
||||
console.log("removed: " + object.name + " " + magicData[0] + " . " + this.items[object.name] + " and sent to " + object.instanceID + " | " + object.instanceName);
|
||||
}
|
||||
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:magicData[0],
|
||||
});
|
||||
//console.log(sentItemStatistics.data)
|
||||
sentItemStatisticsBySlaveID[object.instanceID] = sentItemStatistics;
|
||||
prometheusImportGauge.labels(object.instanceID, object.name).inc(Number(magicData[0]) || 0);
|
||||
|
||||
res.send({
|
||||
name: object.name,
|
||||
count: magicData[0],
|
||||
})
|
||||
} else {
|
||||
res.send({
|
||||
name: object.name,
|
||||
count: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_doleDivisionFactor = {}; //If the server regularly can't fulfill requests, this number grows until it can. Then it slowly shrinks back down.
|
||||
function doleDivider({
|
||||
item,
|
||||
object,
|
||||
db,
|
||||
sentItemStatisticsBySlaveID,
|
||||
config,
|
||||
prometheusDoleFactorGauge,
|
||||
prometheusImportGauge,
|
||||
req,res,
|
||||
}){
|
||||
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
|
||||
|
||||
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);
|
||||
prometheusDoleFactorGauge.labels(object.name).set(_doleDivisionFactor[object.name] || 0);
|
||||
res.send({name:object.name, count:0});
|
||||
//console.log('failure out of ' + object.name + " | " + object.count + " from " + object.instanceID + " ("+object.instanceName+")");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
doleDivider,
|
||||
neuralDole
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
--[[
|
||||
HUGE SHOUTOUT TO HORNWITSER FOR WRITING THIS SEXY AS MODLOADER <3
|
||||
and for not getting mad when i coppyed it :)
|
||||
]]
|
||||
|
||||
local modules = {
|
||||
require("pdnc"),
|
||||
require("doomsday"),
|
||||
require("gui"),
|
||||
}
|
||||
|
||||
script.on_init(function()
|
||||
for i,mod in ipairs(modules) do
|
||||
if mod.on_init then
|
||||
mod.on_init()
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
script.on_load(function()
|
||||
for i,mod in ipairs(modules) do
|
||||
if mod.on_load then
|
||||
mod.on_load()
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
script.on_configuration_changed(function(data)
|
||||
for i,mod in ipairs(modules) do
|
||||
if mod.on_configuration_changed then
|
||||
mod.on_configuration_changed(data)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
local function register_tick_handlers()
|
||||
local ticks = {}
|
||||
for i,mod in ipairs(modules) do
|
||||
print(mod.on_nth_ticks)
|
||||
if mod.on_nth_ticks then
|
||||
mod_ticks = mod.on_nth_ticks
|
||||
for t,f in pairs(mod_ticks) do
|
||||
if not ticks[t] then
|
||||
ticks[t] = {}
|
||||
end
|
||||
|
||||
table.insert(ticks[t], f)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for t,fs in pairs(ticks) do
|
||||
script.on_nth_tick(t, function(event_data)
|
||||
for i,f in ipairs(fs) do
|
||||
f(event_data)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
register_tick_handlers()
|
||||
|
||||
local function register_events()
|
||||
local events = {}
|
||||
for i,mod in ipairs(modules) do
|
||||
if mod.get_events then
|
||||
mod_events = mod.get_events()
|
||||
for e,f in pairs(mod_events) do
|
||||
if not events[e] then
|
||||
events[e] = {}
|
||||
end
|
||||
|
||||
table.insert(events[e], f)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for e,fs in pairs(events) do
|
||||
script.on_event(e, function(event_data)
|
||||
for i,f in ipairs(fs) do
|
||||
f(event_data)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
register_events()
|
||||
|
||||
for i,mod in ipairs(modules) do
|
||||
if mod.add_remote_interfaces then
|
||||
mod.add_remote_interfaces()
|
||||
end
|
||||
end
|
||||
|
||||
for i,mod in ipairs(modules) do
|
||||
if mod.add_commands then
|
||||
mod.add_commands()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--[[
|
||||
|
||||
-- This is a example of what to put at the end of your code, ie doomsday.lua
|
||||
-- replace all the EXAMPLE 's with any name you want, they have to be all the same
|
||||
-- replace tick with number if ticks and function with the function name to call
|
||||
-- everywhere there is a blank space is where you put stuff in
|
||||
|
||||
|
||||
|
||||
local EXAMPLE_init = {}
|
||||
|
||||
local script_events = {
|
||||
--place the here what you would normaly use Event.register for
|
||||
-- Event.register(defines.events.on_player_created, testfunction)
|
||||
-- is the same as
|
||||
-- [defines.events.on_player_created] = testfunction,
|
||||
-- where testfunction is | local function testfunction() { }
|
||||
--[Event] = function,
|
||||
--put stuff here
|
||||
|
||||
}
|
||||
|
||||
EXAMPLE_init.on_nth_ticks = {
|
||||
--place the here what you would normaly use
|
||||
--[tick] = function,
|
||||
--put stuff here
|
||||
|
||||
}
|
||||
|
||||
EXAMPLE_init.on_init = function() -- this runs when Event.core_events.init
|
||||
log("EXAMPLE init")
|
||||
--put stuff here
|
||||
|
||||
global.EXAMPLE_data = global.EXAMPLE_data or script_data -- NO TOUCHY
|
||||
|
||||
end
|
||||
|
||||
EXAMPLE_init.on_load = function() -- this runs when Event.core_events.load
|
||||
log("EXAMPLE load")
|
||||
--put stuff here
|
||||
|
||||
script_data = global.EXAMPLE_data or script_data -- NO TOUCHY
|
||||
end
|
||||
|
||||
EXAMPLE_init.get_events = function()
|
||||
return script_events
|
||||
end
|
||||
|
||||
return EXAMPLE_init
|
||||
]]
|
||||
@@ -0,0 +1,251 @@
|
||||
-- doomsday module. Requires the programmable day-night cycle (pdnc) module to work.
|
||||
-- made by Zr4g0n
|
||||
-- this module currently has issues with the event module and 'on nth tick'.
|
||||
require("pdnc") --is this the best way to do this?
|
||||
global.doomsday_enabled = true
|
||||
global.doomsday = global.doomsday or {} -- used to check if this exists.
|
||||
global.doomsday_start = 8.75 -- in ingame days. Use n.75 to make sure doomsday is at midnight.
|
||||
global.doomsday_pollution = 20 -- amount to be applied per tick
|
||||
global.doomsday_surface = 1
|
||||
global.doomsday_enable_players_online_compensator = false
|
||||
global.doomsday_current_fuzzy_playercount = 1.5 -- start assuming 1.5 players! :V
|
||||
global.doomsday_use_early_death = false
|
||||
global.doomsday_early_death_has_happened = false
|
||||
global.doomsday_use_different_spawn = false
|
||||
global.doomsday_different_spawn = {x = -2000, y = -500}
|
||||
global.doomsday_has_happened = false
|
||||
|
||||
function doomsday_status()
|
||||
stats = {
|
||||
"Time Left: " .. doomsday_time_left(),
|
||||
"global.doomsday_start: " .. global.doomsday_start,
|
||||
"global.doomsday_pollution: " .. global.doomsday_pollution,
|
||||
"global.doomsday_surface: " .. global.doomsday_surface,
|
||||
"global.doomsday_enabled: " .. pdnc_bool_to_string(global.doomsday_enabled),
|
||||
"global.doomsday_enable_players_online_compensator: " .. pdnc_bool_to_string(global.doomsday_enable_players_online_compensator),
|
||||
"global.doomsday_current_fuzzy_playercount: " .. global.doomsday_current_fuzzy_playercount,
|
||||
"global.doomsday_use_early_death: " .. pdnc_bool_to_string(global.doomsday_use_early_death),
|
||||
"global.doomsday_early_death_has_happened: " .. pdnc_bool_to_string(global.doomsday_early_death_has_happened),
|
||||
"global.doomsday_use_different_spawn: " .. pdnc_bool_to_string(global.doomsday_use_different_spawn),
|
||||
"global.doomsday_different_spawn: x = " .. global.doomsday_different_spawn.x .. ", y = " .. global.doomsday_different_spawn.y,
|
||||
"doomsday_has_happened: " .. pdnc_bool_to_string(global.doomsday_has_happened),
|
||||
}
|
||||
return stats
|
||||
end
|
||||
|
||||
function doomsday_console_status()
|
||||
--logs to console, used for debuging
|
||||
log("Time Left: " .. doomsday_time_left())
|
||||
log("global.doomsday_start: " .. global.doomsday_start)
|
||||
log("global.doomsday_pollution: " .. global.doomsday_pollution)
|
||||
log("global.doomsday_surface: " .. global.doomsday_surface)
|
||||
log("global.doomsday_enabled: " .. pdnc_bool_to_string(global.doomsday_enabled))
|
||||
log("global.doomsday_enable_players_online_compensator: " .. pdnc_bool_to_string(global.doomsday_enable_players_online_compensator))
|
||||
log("global.doomsday_current_fuzzy_playercount: " .. global.doomsday_current_fuzzy_playercount)
|
||||
log("global.doomsday_use_early_death: " .. pdnc_bool_to_string(global.doomsday_use_early_death))
|
||||
log("global.doomsday_early_death_has_happened: " .. pdnc_bool_to_string(global.doomsday_early_death_has_happened))
|
||||
log("global.doomsday_use_different_spawn: " .. pdnc_bool_to_string(global.doomsday_use_different_spawn))
|
||||
log("global.doomsday_different_spawn: x = " .. global.doomsday_different_spawn.x .. ", y = " .. global.doomsday_different_spawn.y)
|
||||
log("doomsday_has_happened: " .. pdnc_bool_to_string(global.doomsday_has_happened))
|
||||
return stats
|
||||
end
|
||||
|
||||
function doomsday_toggle()
|
||||
global.doomsday_enabled = not global.doomsday_enabled
|
||||
doomsday_status()
|
||||
end
|
||||
|
||||
function doomsday_setup()
|
||||
if global.doomsday_use_different_spawn then
|
||||
doomsday_activate_different_spawn()
|
||||
end
|
||||
end
|
||||
|
||||
function doomsday_early_death()
|
||||
local radius = 512
|
||||
local pollution = 2500
|
||||
doomsday_pollute(radius*0.66,pollution,12)
|
||||
doomsday_pollute(radius*1.00,pollution,24)
|
||||
doomsday_pollute(radius*1.50,pollution,36)
|
||||
global.doomsday_early_death_has_happened = true
|
||||
end
|
||||
|
||||
function doomsday_activate_different_spawn()
|
||||
game.forces["player"].set_spawn_position(global.doomsday_different_spawn, game.surfaces[global.doomsday_surface])
|
||||
end
|
||||
|
||||
function doomsday_core()
|
||||
local current_time = game.tick / game.surfaces[global.doomsday_surface].ticks_per_day
|
||||
local x = current_time * 6.2831853 --2pi
|
||||
local returnvalue = 0
|
||||
if(global.doomsday_enable_players_online_compensator)then
|
||||
doomsday_players_online_compensator()
|
||||
end
|
||||
|
||||
if global.doomsday_use_early_death
|
||||
and (current_time > 1)
|
||||
and not global.doomsday_early_death_has_happened then
|
||||
doomsday_early_death()
|
||||
end
|
||||
|
||||
local x = current_time * 6.2831853 --2pi
|
||||
local returnvalue = 0
|
||||
if (current_time < global.doomsday_start) then
|
||||
returnvalue = math.pow(pdnc_c_boxy(x), (1 + current_time / 4))
|
||||
-- days become darker over time towards n^6.125
|
||||
elseif (current_time < global.doomsday_start + 1) then
|
||||
returnvalue = doomsday_pollution_zero_hour(current_time)
|
||||
if not global.doomsday_has_happened then
|
||||
global.doomsday_has_happened = true
|
||||
log("Doomsday activated at tick: " .. game.tick)
|
||||
end
|
||||
else
|
||||
global.pdnc_enable_brightness_limit = true
|
||||
returnvalue = math.pow(pdnc_c_boxy(x), 6.125)--*0.5
|
||||
end
|
||||
return pdnc_scaler(returnvalue)
|
||||
end
|
||||
|
||||
function doomsday_pollution_zero_hour(current_time)
|
||||
local radius = 128 --make global
|
||||
local pollution = global.doomsday_pollution -- total pollution applied per tick
|
||||
local nodes = 5 -- the number of nodes to spread
|
||||
doomsday_pollute(radius*0.66,pollution,nodes*0.66)
|
||||
doomsday_pollute(radius*1.00,pollution,nodes*1.00)
|
||||
doomsday_pollute(radius*1.50,pollution,nodes*1.50)
|
||||
return math.pow(((global.doomsday_start + 1) - current_time), 7)
|
||||
end
|
||||
|
||||
|
||||
function doomsday_normal_curve(x)
|
||||
return (1+ ((math.sin(x) + (0.111 * math.sin(3 * x))) * 1.124859392575928))/2
|
||||
-- magic numbers to make it scale to (-1, 1)
|
||||
end
|
||||
|
||||
function doomsday_pollute(radius,pollution,nodes) -- spawn a ring of pollution based on radius, amount of pollution and number of points in the ring.
|
||||
local p = global.pdnc_stepsize * pollution -- needed to make it 'step size independant'
|
||||
p = p / nodes
|
||||
local position = {x = 0.0, y = 0.0}
|
||||
if global.doomsday_use_different_spawn then
|
||||
position = global.doomsday_different_spawn
|
||||
end
|
||||
--game.surfaces[global.doomsday_surface].pollute(position, p) --circle + center point
|
||||
local step = (math.pi * 2) / (nodes - 1)
|
||||
for i=0, (nodes - 1) do
|
||||
position = {x = math.sin(step*i)*radius, y = math.cos(step*i)*radius}
|
||||
game.surfaces[global.doomsday_surface].pollute(position, p)
|
||||
end
|
||||
end
|
||||
|
||||
function doomsday_time_left()
|
||||
if (global.doomsday_start > 0)then
|
||||
local ticks_until_doomsday = game.surfaces[global.doomsday_surface].ticks_per_day * global.doomsday_start
|
||||
local ticks = ticks_until_doomsday - game.tick
|
||||
if (ticks >= 0) then
|
||||
local seconds = math.floor(ticks/ 60)
|
||||
local minutes = math.floor(seconds / 60)
|
||||
local hours = math.floor(minutes / 60)
|
||||
local days = math.floor(hours / 24)
|
||||
return(string.format("-" .. "%d:%02d:%02d:%02d", hours, minutes % 60, seconds % 60, ticks % 60))
|
||||
else
|
||||
ticks = ticks * -1
|
||||
local seconds = math.floor(ticks / 60)
|
||||
local minutes = math.floor(seconds / 60)
|
||||
local hours = math.floor(minutes / 60)
|
||||
local days = math.floor(hours / 24)
|
||||
return(string.format("%d:%02d:%02d:%02d", hours, minutes % 60, seconds % 60, ticks % 60) .. " ago...")
|
||||
end
|
||||
else
|
||||
return("Nothing to see here, move along! No doomsday here, nope!")
|
||||
end
|
||||
end
|
||||
|
||||
function doomsday_time_left_with_ticks()
|
||||
local tick = (((game.surfaces[global.doomsday_surface].ticks_per_day * global.doomsday_start) - game.tick)%60)
|
||||
return doomsday_time_left() .. ":" .. string.format("%02d", tick)
|
||||
end
|
||||
|
||||
function doomsday_players_online_compensator()
|
||||
local target = #game.connected_players
|
||||
local current = global.doomsday_current_fuzzy_playercount
|
||||
local step_size = 0.02 -- (at 21 ticks per iteration, that's 17.5sec per player change.
|
||||
|
||||
if(current < 0)then
|
||||
game.print("Error; 'current' number of players' is less than 0!")
|
||||
end
|
||||
if(target < 0)then
|
||||
game.print("Error; 'number of players' is less than 0!")
|
||||
end
|
||||
|
||||
if(current == target) then
|
||||
-- do nothing, this should be most of the time
|
||||
elseif ( current < target) then
|
||||
current = current + step_size
|
||||
elseif ( current > target) then
|
||||
current = current - step_size
|
||||
else
|
||||
-- do nothing?
|
||||
end
|
||||
local modifier = 20/(0.1 * current * current + 6) -- Nice, smooth curve that should hopefully give boost equal to the playercount!
|
||||
game.forces.player.character_running_speed_modifier = modifier
|
||||
global.doomsday_current_fuzzy_playercount = current
|
||||
end
|
||||
|
||||
--[[
|
||||
function reduce_brightness(n)
|
||||
global.pdnc_max_brightness = 1 - ((global.pdnc_current_time / global.pdnc_doomsday_start)*n)
|
||||
if(global.pdnc_max_brightness < n) then
|
||||
global.pdnc_max_brightness = n
|
||||
end
|
||||
end
|
||||
]]
|
||||
|
||||
|
||||
|
||||
-- This is a example of what to put at the end of your code, ie doomsday.lua
|
||||
-- replaze EXAMPLE with any name you want
|
||||
-- replace tick with number if ticks and function with the function name to call
|
||||
-- everywhere there is a blank space is where you put stuff in
|
||||
|
||||
local doomsday_init = {}
|
||||
|
||||
local script_events = {
|
||||
--place the here what you would normaly use Event.register for
|
||||
-- Event.register(defines.events.on_player_created, testfunction)
|
||||
-- is the same as
|
||||
-- [defines.events.on_player_created] = testfunction,
|
||||
-- where testfunction is | local functuin testfunction() { }
|
||||
--[Event] = function,
|
||||
--put stuff here
|
||||
|
||||
}
|
||||
|
||||
doomsday_init.on_nth_ticks = {
|
||||
--place the here what you would normaly use
|
||||
--[tick] = function,
|
||||
--put stuff here
|
||||
--[60] = doomsday_console_status, -- prints status to console ever second
|
||||
|
||||
}
|
||||
|
||||
doomsday_init.on_init = function() -- this runs when Event.core_events.init
|
||||
log("doomsday init")
|
||||
--put stuff here
|
||||
doomsday_setup()
|
||||
global.doomsday_data = global.doomsday_data or script_data -- NO TOUCHY
|
||||
end
|
||||
|
||||
doomsday_init.on_load = function() -- this runs when Event.core_events.load
|
||||
log("doomsday load")
|
||||
--put stuff here
|
||||
script_data = global.doomsday_data or script_data -- NO TOUCHY
|
||||
end
|
||||
|
||||
doomsday_init.get_events = function()
|
||||
return script_events
|
||||
end
|
||||
|
||||
return doomsday_init
|
||||
|
||||
--Event.register(-global.pdnc_stepsize, doomsday_pdnc_program) --intentionally using the PDNC stepsize so the functions sync this can be skipped, since it's called from pdnc!
|
||||
--Event.register(Event.core_events.init, doomsday_setup)
|
||||
@@ -0,0 +1,266 @@
|
||||
local mod_gui = require 'mod-gui'
|
||||
|
||||
-- ADDS DOOMSDAY STATS BUTTON
|
||||
local function gui_update_DOOM(player)
|
||||
local gui = mod_gui.get_frame_flow(player)
|
||||
local frame = gui.doomsday_stats
|
||||
if not frame then
|
||||
return
|
||||
end
|
||||
|
||||
frame.clear()
|
||||
for i,stat in ipairs(doomsday_status()) do
|
||||
frame.add{
|
||||
type = "label",
|
||||
caption = stat
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
local function toggle_frame_DOOM(player)
|
||||
local gui = mod_gui.get_frame_flow(player)
|
||||
local frame = gui.doomsday_stats
|
||||
|
||||
if frame then
|
||||
frame.destroy()
|
||||
return
|
||||
end
|
||||
|
||||
frame = gui.add{
|
||||
type = "frame",
|
||||
name = "doomsday_stats",
|
||||
direction = "vertical",
|
||||
caption = "Doomsday stats",
|
||||
style = mod_gui.frame_style,
|
||||
}
|
||||
|
||||
frame.style.horizontally_stretchable = false
|
||||
frame.style.vertically_stretchable = false
|
||||
gui_update_DOOM(player)
|
||||
end
|
||||
|
||||
local function get_sprite_button_DOOM(player)
|
||||
local button_flow = mod_gui.get_button_flow(player)
|
||||
button_DOOM = button_flow.doomsday_stats_button
|
||||
|
||||
if button_DOOM then
|
||||
button_DOOM.destroy()
|
||||
end
|
||||
|
||||
if player.admin then
|
||||
button_DOOM = button_flow.add{
|
||||
type = "sprite-button",
|
||||
name = "doomsday_stats_button",
|
||||
sprite = "item/raw-fish",
|
||||
style = mod_gui.button_style,
|
||||
tooltip = "Show debug stats for doomsday",
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
--ADDS PDNC STATS BUTTON
|
||||
local function gui_update_PDNC(player)
|
||||
local gui = mod_gui.get_frame_flow(player)
|
||||
local frame_PDNC = gui.PDNC_stats
|
||||
if not frame_PDNC then
|
||||
return
|
||||
end
|
||||
|
||||
frame_PDNC.clear()
|
||||
for i,stat in ipairs(pdnc_extended_status()) do
|
||||
frame_PDNC.add{
|
||||
type = "label",
|
||||
caption = stat
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
local function toggle_frame_PDNC(player)
|
||||
local gui = mod_gui.get_frame_flow(player)
|
||||
local frame_PDNC = gui.PDNC_stats
|
||||
|
||||
if frame_PDNC then
|
||||
frame_PDNC.destroy()
|
||||
return
|
||||
end
|
||||
|
||||
frame_PDNC = gui.add{
|
||||
type = "frame",
|
||||
name = "PDNC_stats",
|
||||
direction = "vertical",
|
||||
caption = "PDNC stats",
|
||||
style = mod_gui.frame_style,
|
||||
}
|
||||
|
||||
frame_PDNC.style.horizontally_stretchable = false
|
||||
frame_PDNC.style.vertically_stretchable = false
|
||||
gui_update_PDNC(player)
|
||||
end
|
||||
|
||||
local function get_sprite_button_PDNC(player)
|
||||
local button_flow = mod_gui.get_button_flow(player)
|
||||
local button_PDNC = button_flow.PDNC_stats_button
|
||||
|
||||
if button_PDNC then
|
||||
button_PDNC.destroy()
|
||||
end
|
||||
|
||||
if player.admin then
|
||||
button_PDNC = button_flow.add{
|
||||
type = "sprite-button",
|
||||
name = "PDNC_stats_button",
|
||||
sprite = "item/raw-fish",
|
||||
style = mod_gui.button_style,
|
||||
tooltip = "Show debug stats for PDNC",
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
-- ADDS DOOMSDAY TIME LEFT COUNTER
|
||||
local function gui_update_counter(player)
|
||||
local gui = player.gui.screen --mod_gui.get_frame_flow(player)
|
||||
local frame_counter = gui.doomsday_counter
|
||||
if not frame_counter then
|
||||
return
|
||||
end
|
||||
|
||||
frame_counter.clear()
|
||||
frame_counter.add{
|
||||
type = "label",
|
||||
caption = doomsday_time_left()
|
||||
}
|
||||
frame_counter.location = {player.display_resolution.width/2 - 59, 10}
|
||||
frame_counter.visible = true
|
||||
end
|
||||
|
||||
local function toggle_frame_counter(player)
|
||||
--local player = player
|
||||
local gui = player.gui.screen --mod_gui.get_frame_flow(player)
|
||||
local frame_counter = gui.doomsday_counter
|
||||
-- if frame_counter then
|
||||
-- frame_counter.destroy()
|
||||
-- return
|
||||
-- end
|
||||
|
||||
frame_counter = gui.add{
|
||||
type = "frame",
|
||||
name = "doomsday_counter",
|
||||
direction = "horizontal",
|
||||
--caption = "Doomsday counter",
|
||||
style = mod_gui.frame_style,
|
||||
}
|
||||
|
||||
--frame_counter.style.horizontally_stretchable = false
|
||||
frame_counter.style.vertically_stretchable = false
|
||||
frame_counter.visible = false
|
||||
gui_update_counter(player)
|
||||
end
|
||||
|
||||
local function update_buttons(event)
|
||||
local player = game.players[event.player_index]
|
||||
if not (player and player.valid) then
|
||||
return
|
||||
end
|
||||
|
||||
get_sprite_button_DOOM(player)
|
||||
get_sprite_button_PDNC(player)
|
||||
|
||||
-- Make sure the frames are not visible to players
|
||||
if not player.admin then
|
||||
local gui = mod_gui.get_frame_flow(player)
|
||||
local frame_PDNC = gui.PDNC_stats
|
||||
|
||||
if frame_PDNC then
|
||||
frame_PDNC.destroy()
|
||||
end
|
||||
|
||||
local frame_DOOM = gui.doomsday_stats
|
||||
|
||||
if frame_DOOM then
|
||||
frame_DOOM.destroy()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ON GUI CLICK AND OTHERS
|
||||
local function on_gui_click(event)
|
||||
local gui = event.element
|
||||
local player = game.players[event.player_index]
|
||||
if not (player and player.valid and gui and gui.valid) then
|
||||
return
|
||||
end
|
||||
|
||||
if gui.name == "doomsday_stats_button" then
|
||||
toggle_frame_DOOM(player)
|
||||
end
|
||||
if gui.name == "PDNC_stats_button" then
|
||||
toggle_frame_PDNC(player)
|
||||
end
|
||||
end
|
||||
|
||||
local function on_player_created(event)
|
||||
local player = game.players[event.player_index]
|
||||
if not (player and player.valid) then
|
||||
return
|
||||
end
|
||||
|
||||
update_buttons(event)
|
||||
toggle_frame_counter(player)
|
||||
end
|
||||
|
||||
local function gui_update_all()
|
||||
for _, player in pairs(game.players) do
|
||||
if player and player.valid then
|
||||
gui_update_DOOM(player)
|
||||
gui_update_PDNC(player)
|
||||
gui_update_counter(player)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function on_gui_tick(event)
|
||||
gui_update_all()
|
||||
end
|
||||
|
||||
local doomsdaygui_init = {}
|
||||
local script_events = {
|
||||
--place the here what you would normaly use Event.register for
|
||||
-- Event.register(defines.events.on_player_created, testfunction)
|
||||
-- is the same as
|
||||
-- [defines.events.on_player_created] = testfunction,
|
||||
-- where testfunction is | local functuin testfunction() { }
|
||||
--[Event] = function,
|
||||
--put stuff here
|
||||
[defines.events.on_gui_click] = on_gui_click,
|
||||
[defines.events.on_player_created] = on_player_created,
|
||||
[defines.events.on_player_promoted] = update_buttons,
|
||||
[defines.events.on_player_demoted] = update_buttons,
|
||||
}
|
||||
|
||||
doomsdaygui_init.on_nth_ticks = {
|
||||
--place the here what you would normaly use
|
||||
--[tick] = function,
|
||||
--put stuff here
|
||||
[1] = on_gui_tick,
|
||||
}
|
||||
|
||||
doomsdaygui_init.on_init = function() -- this runs when Event.core_events.init
|
||||
log("doomsdaygui init")
|
||||
--put stuff here
|
||||
|
||||
global.doomsdaygui_data = global.doomsdaygui_data or script_data -- NO TOUCHY
|
||||
|
||||
end
|
||||
|
||||
doomsdaygui_init.on_load = function() -- this runs when Event.core_events.load
|
||||
log("doomsdaygui load")
|
||||
--put stuff here
|
||||
|
||||
script_data = global.doomsdaygui_data or script_data -- NO TOUCHY
|
||||
end
|
||||
|
||||
doomsdaygui_init.get_events = function()
|
||||
return script_events
|
||||
end
|
||||
|
||||
return doomsdaygui_init
|
||||
@@ -0,0 +1,309 @@
|
||||
-- Third itteration of programmable day-night cycle for Factorio
|
||||
-- Developed by Zr4g0n with help
|
||||
-- Features needed:
|
||||
-- Set 'maximum brightness' from rocket launches cluster wide
|
||||
-- Set 'time until doomsday' to any time, in minutes.
|
||||
-- Set 'evolution target' to any number [0.0, 1.0]
|
||||
-- Changing day-night cycle based on days
|
||||
-- Handle 'next time' elegantly
|
||||
|
||||
-- lamps enable darkness: [0.595 - 0.425] scaled to [0.0 - 0.85] range from [0.0 - 1.0] range
|
||||
global.pdnc = global.pdnc or {}
|
||||
global.pdnc_enabled = true
|
||||
global.pdnc_stepsize = 53 -- also used for script.on_nth_tick. Cannot be changed at runtime!
|
||||
global.pdnc_surface = 1
|
||||
global.pdnc_current_time = 0
|
||||
global.pdnc_current_point = {x = 0, y = 1.0}
|
||||
global.pdnc_last_point = {x = -1, y = 0.0}
|
||||
global.pdnc_max_brightness = 0.5 -- for clusterio
|
||||
global.pdnc_debug = false
|
||||
global.pdnc_enable_brightness_limit = false
|
||||
global.pdnc_enable_rocket_darkness = false
|
||||
global.pdnc_rockets_launched = 0
|
||||
global.pdnc_rockets_launched_step_size = 0.025
|
||||
global.pdnc_rockets_launched_smooth = 0
|
||||
global.pdnc_min_per_day = 15.0
|
||||
|
||||
function pdnc_setup()
|
||||
--game.surfaces[global.pdnc_surface].ticks_per_day = pdnc_min_to_ticks(10.0)
|
||||
doomsday_setup()
|
||||
end
|
||||
|
||||
-- Returns true if the player is an admin
|
||||
function check_admin(ctx)
|
||||
local player = game.players[ctx.player_index]
|
||||
if not player.admin then
|
||||
player.print("Only admins can use /"..ctx.name)
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function pdnc_toggle_debug(ctx)
|
||||
if not check_admin(ctx) then return end
|
||||
global.pdnc_debug = not global.pdnc_debug
|
||||
pdnc_print_status(ctx)
|
||||
end
|
||||
|
||||
function pdnc_toggle(ctx)
|
||||
if not check_admin(ctx) then return end
|
||||
global.pdnc_enabled = not global.pdnc_enabled
|
||||
pdnc_print_status(ctx)
|
||||
end
|
||||
|
||||
function pdnc_print_status(ctx)
|
||||
local player = game.players[ctx.player_index]
|
||||
if(global.pdnc_enabled)then
|
||||
player.print("PDNC is enabled")
|
||||
else
|
||||
player.print("PDNC is disabled")
|
||||
end
|
||||
|
||||
if(global.pdnc_debug)then
|
||||
player.print("PDNC debug is enabled")
|
||||
pdnc_extended_status()
|
||||
else
|
||||
player.print("PDNC debug is disabled")
|
||||
end
|
||||
end
|
||||
|
||||
function pdnc_extended_status()
|
||||
stats = {
|
||||
"global.pdnc_stepsize: " .. global.pdnc_stepsize,
|
||||
"global.pdnc_surface: " .. global.pdnc_surface,
|
||||
"global.pdnc_current_time: " .. global.pdnc_current_time,
|
||||
"global.pdnc_max_brightness: " .. global.pdnc_max_brightness,
|
||||
"global.pdnc_enable_brightness_limit: " .. pdnc_bool_to_string(global.pdnc_enable_brightness_limit),
|
||||
"global.pdnc_enable_rocket_darkness: " .. pdnc_bool_to_string(global.pdnc_enable_rocket_darkness),
|
||||
"global.pdnc_rockets_launched: " .. global.pdnc_rockets_launched,
|
||||
"global.pdnc_rockets_launched_step_size: " .. global.pdnc_rockets_launched_step_size,
|
||||
"global.pdnc_rockets_launched_smooth: " .. global.pdnc_rockets_launched_smooth,
|
||||
"ticks per day: " .. game.surfaces[global.pdnc_surface].ticks_per_day,
|
||||
"current tick: " .. game.tick,
|
||||
}
|
||||
return stats
|
||||
end
|
||||
|
||||
function pdnc_bool_to_string(b)
|
||||
if(b)then
|
||||
return "true"
|
||||
else
|
||||
return "false"
|
||||
end
|
||||
end
|
||||
|
||||
function pdnc_core()
|
||||
if(global.pdnc_enabled)then
|
||||
doomsday_core()
|
||||
local current_surface = game.surfaces[global.pdnc_surface]
|
||||
pdnc_freeze_check(current_surface)
|
||||
current_surface.ticks_per_day = pdnc_min_to_ticks(global.pdnc_min_per_day) -- move this somewhere else; doesn't need to run every nth tick!
|
||||
global.pdnc_current_time = game.tick / current_surface.ticks_per_day
|
||||
|
||||
-- get current Y coordinate. X = 0 always.
|
||||
local current_point = {x = global.pdnc_current_time, y = pdnc_program(global.pdnc_current_time)}
|
||||
-- get next Y coordinate, X = 'step size'
|
||||
local next_point = {x = (global.pdnc_current_time + (global.pdnc_stepsize/current_surface.ticks_per_day)), y = pdnc_program(global.pdnc_current_time + (global.pdnc_stepsize/current_surface.ticks_per_day))}
|
||||
|
||||
local top_point = pdnc_intersection_top(current_point, next_point)
|
||||
local bot_point = pdnc_intersection_bot(current_point, next_point)
|
||||
|
||||
-- the order is dusk - evening - morning - dawn. They *must* be in that order and they cannot be equal
|
||||
if(top_point < bot_point) then -- dusk -> evening
|
||||
pdnc_cleanup_last_tick(current_surface)
|
||||
current_surface.evening = bot_point - current_point.x
|
||||
current_surface.dusk = top_point - current_point.x
|
||||
elseif(top_point > bot_point) then -- morning -> dawn
|
||||
pdnc_cleanup_last_tick(current_surface)
|
||||
current_surface.morning = bot_point - current_point.x
|
||||
current_surface.dawn = top_point - current_point.x
|
||||
elseif(top_point == bot_point) then
|
||||
pdnc_debug_message("PDNC: Top and bot point equal")
|
||||
-- no cleanup is done here
|
||||
-- if the points are equal, use last value until not equal
|
||||
-- this should never be reached unless the pdnc_program() is broken.
|
||||
else
|
||||
pdnc_debug_message("Top and bot not different nor equal. probably a NaN error")
|
||||
pdnc_debug_message("bot_point: " .. bot_point)
|
||||
pdnc_debug_message("top_point: " .. top_point)
|
||||
-- this should never be reached.
|
||||
end
|
||||
else
|
||||
end
|
||||
end
|
||||
|
||||
function pdnc_cleanup_last_tick(current_surface)
|
||||
current_surface.daytime = 0
|
||||
-- must be in this spesific order to
|
||||
-- preserve the order at all times
|
||||
-- dusk < evening < morning < dawn.
|
||||
-- DO NOT CHANGE THE ORDER!
|
||||
current_surface.dusk = -999999999999
|
||||
current_surface.dawn = 999999999999
|
||||
current_surface.evening = -999999999998
|
||||
current_surface.morning = 999999999998
|
||||
end
|
||||
|
||||
function pdnc_disable_and_reset(ctx)
|
||||
if not check_admin(ctx) then return end
|
||||
local current_surface = game.surfaces[global.pdnc_surface]
|
||||
pdnc_cleanup_last_tick(current_surface)
|
||||
-- DO NOT CHANGE THIS ORDER!
|
||||
current_surface.evening = 0.45
|
||||
current_surface.morning = 0.55
|
||||
current_surface.dusk = 0.25
|
||||
current_surface.dawn = 0.75
|
||||
current_surface.ticks_per_day = 25000
|
||||
global.pdnc_enabled = false
|
||||
pdnc_debug_message("PDNC Programable Day-Night Cycle disabled, normal day-night cycle enabled")
|
||||
end
|
||||
|
||||
function pdnc_freeze_check(current_surface)
|
||||
if(current_surface.freeze_daytime)then
|
||||
current_surface.freeze_daytime = false
|
||||
game.print("Can't use freeze_daytime while programmable day-night cycle is active; time has been unfrozen")
|
||||
end
|
||||
end
|
||||
|
||||
function pdnc_program(x)
|
||||
return pdnc_scaler(pdnc_c_boxy(x*6.28318530717958647692)) -- that's Tau, aka 2xPi
|
||||
end
|
||||
|
||||
function pdnc_c_boxy(x)
|
||||
return pdnc_normalize((math.sin(x) + (0.111 * math.sin(3 * x))) * 1.124859392575928)
|
||||
-- magic numbers to make it scale to (-1, 1)
|
||||
end
|
||||
|
||||
function pdnc_normalize(n)
|
||||
return (n + 1)/2
|
||||
end
|
||||
|
||||
function pdnc_scaler(r) -- a bit messy, but simplifies a lot elsewhere
|
||||
if(pdnc_check_valid(r, "pdnc_scaler"))then
|
||||
|
||||
local a = 1
|
||||
local b = 1
|
||||
|
||||
if(global.pdnc_enable_brightness_limit) then
|
||||
a = global.pdnc_max_brightness
|
||||
end
|
||||
|
||||
if(global.pdnc_enable_rocket_darkness) then
|
||||
b = 1 - pdnc_rocket_launch_darkness()
|
||||
end
|
||||
|
||||
return r * 0.85 * a * b -- 0 -> 0.85 is the 'brightness range' of factorio
|
||||
end
|
||||
end
|
||||
|
||||
function pdnc_intersection (s1, e1, s2, e2)
|
||||
local d = (s1.x - e1.x) * (s2.y - e2.y) - (s1.y - e1.y) * (s2.x - e2.x)
|
||||
local a = s1.x * e1.y - s1.y * e1.x
|
||||
local b = s2.x * e2.y - s2.y * e2.x
|
||||
local x = (a * (s2.x - e2.x) - (s1.x - e1.x) * b) / d
|
||||
--local y = (a * (s2.y - e2.y) - (s1.y - e1.y) * b) / d
|
||||
return x--, y
|
||||
end
|
||||
|
||||
function pdnc_intersection_top (s2, e2)
|
||||
local s1, e1 = {x = -999999999, y = 0.85}, {x = 999999999, y = 0.85}
|
||||
return pdnc_intersection (s1, e1, s2, e2)
|
||||
end
|
||||
|
||||
function pdnc_intersection_bot (s2, e2)
|
||||
local s1, e1 = {x = -999999999, y = 0.0}, {x = 999999999, y = 0.0}
|
||||
return pdnc_intersection (s1, e1, s2, e2)
|
||||
end
|
||||
|
||||
function pdnc_set_max_brightness(n)
|
||||
if(pdnc_check_valid(n, "pdnc_set_max_brightness")) then
|
||||
global.pdnc_max_brightness = n
|
||||
pdnc_debug_message("global.pdnc_max_brightness set to " .. global.pdnc_max_brightness)
|
||||
end
|
||||
end
|
||||
|
||||
function pdnc_min_to_ticks(m)
|
||||
return 60*60*m
|
||||
end
|
||||
|
||||
function pdnc_rocket_launch_counter()
|
||||
global.pdnc_rockets_launched = 1 + global.pdnc_rockets_launched
|
||||
end
|
||||
|
||||
function pdnc_rocket_launch_darkness()
|
||||
if (global.pdnc_rockets_launched_smooth < global.pdnc_rockets_launched)then
|
||||
global.pdnc_rockets_launched_smooth = global.pdnc_rockets_launched_step_size + global.pdnc_rockets_launched_smooth
|
||||
end
|
||||
return (1 - (50/(global.pdnc_rockets_launched_smooth+50)))
|
||||
end
|
||||
|
||||
function pdnc_check_valid(n, s)
|
||||
if (n == nil) then
|
||||
pdnc_debug_message(s .. " set to nil! Set to 1.0 instead")
|
||||
return false
|
||||
elseif (n < 0) then
|
||||
pdnc_debug_message(s .. " cannot be " .. n .. " limited to 0.0 instead")
|
||||
return false
|
||||
elseif (n > 1) then
|
||||
pdnc_debug_message(s .. " cannot be " .. n .. " limited to 1.0 instead")
|
||||
return false
|
||||
elseif (n ~= n) then
|
||||
pdnc_debug_message(s .. " cannot be " .. n .. " since it's not a valid number!")
|
||||
else return true
|
||||
end
|
||||
end
|
||||
|
||||
function pdnc_debug_message(s)
|
||||
if(global.pdnc_debug) then
|
||||
game.print(s)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local PDNC_init = {}
|
||||
|
||||
local script_events = {
|
||||
--place the here what you would normaly use Event.register for
|
||||
-- Event.register(defines.events.on_player_created, testfunction)
|
||||
-- is the same as
|
||||
-- [defines.events.on_player_created] = testfunction,
|
||||
-- where testfunction is | local functuin testfunction() { }
|
||||
--[Event] = function,
|
||||
--put stuff here
|
||||
|
||||
}
|
||||
|
||||
PDNC_init.on_nth_ticks = {
|
||||
--place the here what you would normaly use
|
||||
--[tick] = function,
|
||||
--put stuff here
|
||||
[global.pdnc_stepsize] = pdnc_core,
|
||||
}
|
||||
|
||||
PDNC_init.add_commands = function()
|
||||
commands.add_command("pdnc", "gives PDNC's status", pdnc_print_status)
|
||||
commands.add_command("pdnc_toggle", "toggles pdnc", pdnc_toggle)
|
||||
commands.add_command("pdnc_toggle_debug", "toggles pdnc debug mode", pdnc_toggle_debug)
|
||||
commands.add_command("pdnc_disable_and_reset", "Disabled pdnc and resets the dnc to the vanilla default", pdnc_disable_and_reset)
|
||||
end
|
||||
|
||||
PDNC_init.on_init = function() -- this runs when Event.core_events.init
|
||||
log("PDNC init")
|
||||
--put stuff here
|
||||
global.PDNC_data = global.PDNC_data or script_data -- NO TOUCHY
|
||||
|
||||
end
|
||||
|
||||
PDNC_init.on_load = function() -- this runs when Event.core_events.load
|
||||
log("PDNC load")
|
||||
|
||||
--put stuff here
|
||||
|
||||
script_data = global.PDNC_data or script_data -- NO TOUCHY
|
||||
end
|
||||
|
||||
PDNC_init.get_events = function()
|
||||
return script_events
|
||||
end
|
||||
|
||||
return PDNC_init
|
||||
@@ -22,12 +22,15 @@ module.exports = class remoteCommands {
|
||||
words.shift();
|
||||
words.shift();
|
||||
words.shift();
|
||||
let message = words.join(" ").replace(/[";]/g, " ").replace(/[']/g,"").replace("/shout ", "").replace("!shout", "");
|
||||
let message = words.join(" ").replace(/[";]/g, " ").replace(/[']/g,"").replace("/shout ", "").replace("!shout ","").replace(/\[gps=.*?\]/g, "").replace(/\[train=.*?\]/g, "");
|
||||
|
||||
let instance = await this.getInstanceName(data.instanceID);
|
||||
|
||||
this.messageInterface("/silent-command game.print('[" + instance + "] " + message + "')");
|
||||
}
|
||||
let instance = await this.getInstanceName(data.instanceID);
|
||||
if(data.data.includes("[gps=")){
|
||||
//Nothing
|
||||
}else{
|
||||
this.messageInterface("/silent-command game.print('[" + instance + "] " + message + "')");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
-- Lua cheatsheet
|
||||
|
||||
/c game.write_file("test.txt",serpent.block(game.players["Danielv123"].get_quickbar().get_contents()))
|
||||
/c game.write_file("test.txt",serpent.block(game.players["Danielv123"].get_inventory(defines.inventory.player_main).get_contents()))
|
||||
/c game.write_file("test.txt",serpent.block(game.players["Danielv123"].get_inventory(defines.inventory.character_main).get_contents()))
|
||||
|
||||
-- print JSON of entire inventory and request slots to file
|
||||
-- TODO: Get requester slot data as well
|
||||
@@ -34,7 +34,7 @@ for a,b in pairs(game.players) do
|
||||
else
|
||||
notFirst = true
|
||||
end
|
||||
local inventory = game.players[a].get_inventory(defines.inventory.player_main).get_contents()
|
||||
local inventory = game.players[a].get_inventory(defines.inventory.character_main).get_contents()
|
||||
local quickbar = game.players[a].get_quickbar().get_contents()
|
||||
--[[ Get contents of requester slots into a table ]]
|
||||
local requests = {}
|
||||
@@ -95,13 +95,13 @@ enclose in ''
|
||||
]]
|
||||
|
||||
-- minified strings (newest on the top)
|
||||
local a='+"'"+'{"players":{'+"'"+'local b=false;for c,d in pairs(game.players)do if d.connected then if b then a=a..'+"'"+','+"'"+'else b=true end;local e=game.players[c].get_inventory(defines.inventory.player_main).get_contents()local f=game.players[c].get_quickbar().get_contents()local g={}for h=1,game.players[c].force.character_logistic_slot_count do g[h]=game.players[c].character.get_request_slot(h)end;a=a..'+"'"+'"'+"'"+'..c..'+"'"+'":{"inventory":{'+"'"+'local i=false;for j,k in pairs(e)do if i then a=a..'+"'"+','+"'"+'else i=true end;a=a..'+"'"+'"'+"'"+'..j..'+"'"+'":'+"'"+'..k end;for j,k in pairs(f)do if i then a=a..'+"'"+','+"'"+'else i=true end;a=a..'+"'"+'"'+"'"+'..j..'+"'"+'":'+"'"+'..k end;a=a..'+"'"+'},"requestSlots":{'+"'"+'i=false;for j,k in pairs(g)do if i then a=a..'+"'"+','+"'"+'else i=true end;a=a..'+"'"+'"'+"'"+'..k['+"'"+'name'+"'"+']..'+"'"+'":'+"'"+'..k['+"'"+'count'+"'"+']end;a=a..'+"'"+'}}'+"'"+'end end;a=a.."}}"game.write_file("'+outputFile+'",a,true,0)
|
||||
local a='+"'"+'{"players":{'+"'"+'local b=false;for c,d in pairs(game.players)do if d.connected then if b then a=a..'+"'"+','+"'"+'else b=true end;local e=game.players[c].get_inventory(defines.inventory.character_main).get_contents()local f=game.players[c].get_quickbar().get_contents()local g={}for h=1,game.players[c].force.character_logistic_slot_count do g[h]=game.players[c].character.get_request_slot(h)end;a=a..'+"'"+'"'+"'"+'..c..'+"'"+'":{"inventory":{'+"'"+'local i=false;for j,k in pairs(e)do if i then a=a..'+"'"+','+"'"+'else i=true end;a=a..'+"'"+'"'+"'"+'..j..'+"'"+'":'+"'"+'..k end;for j,k in pairs(f)do if i then a=a..'+"'"+','+"'"+'else i=true end;a=a..'+"'"+'"'+"'"+'..j..'+"'"+'":'+"'"+'..k end;a=a..'+"'"+'},"requestSlots":{'+"'"+'i=false;for j,k in pairs(g)do if i then a=a..'+"'"+','+"'"+'else i=true end;a=a..'+"'"+'"'+"'"+'..k['+"'"+'name'+"'"+']..'+"'"+'":'+"'"+'..k['+"'"+'count'+"'"+']end;a=a..'+"'"+'}}'+"'"+'end end;a=a.."}}"game.write_file("'+outputFile+'",a,true,0)
|
||||
-- offline playes
|
||||
local a="{"for b,c in pairs(game.players)do if c.connected then local d=game.players[b].get_inventory(defines.inventory.player_main).get_contents()local e=game.players[b].get_quickbar().get_contents()local f={}for g=1,game.players[b].force.character_logistic_slot_count do f[g]=game.players[b].character.get_request_slot(g)end;a=a..b..":{inventory:{"for h,i in pairs(d)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;for h,i in pairs(e)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;a=a.."},requestSlots:{"for h,i in pairs(f)do a=a.."['+"'"+'"..i["name"].."'+"'"+']:"..i["count"]..", "end;a=a.."}},"end end;a=a.."}"game.print(a)
|
||||
local a="{"for b,c in pairs(game.players)do if c.connected then local d=game.players[b].get_inventory(defines.inventory.character_main).get_contents()local e=game.players[b].get_quickbar().get_contents()local f={}for g=1,game.players[b].force.character_logistic_slot_count do f[g]=game.players[b].character.get_request_slot(g)end;a=a..b..":{inventory:{"for h,i in pairs(d)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;for h,i in pairs(e)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;a=a.."},requestSlots:{"for h,i in pairs(f)do a=a.."['+"'"+'"..i["name"].."'+"'"+']:"..i["count"]..", "end;a=a.."}},"end end;a=a.."}"game.print(a)
|
||||
-- multiple players
|
||||
local a="{"for b,c in pairs(game.players)do local d=game.players[b].get_inventory(defines.inventory.player_main).get_contents()local e=game.players[b].get_quickbar().get_contents()local f={}for g=1,game.players[b].force.character_logistic_slot_count do f[g]=game.players[b].character.get_request_slot(g)end;a=a..b..":{inventory:{"for h,i in pairs(d)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;for h,i in pairs(e)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;a=a.."},requestSlots:{"for h,i in pairs(f)do a=a.."['+"'"+'"..i["name"].."'+"'"+']:"..i["count"]..", "end;a=a.."}},"end;a=a.."}"game.print(a)
|
||||
local a=game.players["Danielv123"].get_inventory(defines.inventory.player_main).get_contents()local b=game.players["Danielv123"].get_quickbar().get_contents()local c={}for d=1,game.players["Danielv123"].force.character_logistic_slot_count do c[d]=game.players["Danielv123"].character.get_request_slot(d)end;local e="{inventory:{"for f,g in pairs(a)do e=e.."['+"'"+'"..f.."'+"'"+']:"..g..","end;for f,g in pairs(b)do e=e.."['+"'"+'"..f.."'+"'"+']:"..g..","end;e=e.."},requestSlots:{"for f,g in pairs(c)do e=e.."['+"'"+'"..g["name"].."'+"'"+']:"..g["count"]..", "end;e=e.."}}"game.print(e)
|
||||
local inventory = game.players["Danielv123"].get_inventory(defines.inventory.player_main).get_contents()local quickbar = game.players["Danielv123"].get_quickbar().get_contents()local requests = {}for i = 1, game.player.force.character_logistic_slot_count do requests[i] = game.players["Danielv123"].character.get_request_slot(i) end local o = "{inventory:{" for k,v in pairs(inventory) do o = o.."['+"'"+'"..k.."'+"'"+']:"..v.."," end for k,v in pairs(quickbar) do o = o.."['+"'"+'"..k.."'+"'"+']:"..v.."," end o = o.."},requestSlots:{" for k,v in pairs(requests) do o = o.."['+"'"+'"..v["name"].."'+"'"+']:"..v["count"]..", " end o = o.."}}" game.write_file("test.txt", o,true,0)
|
||||
local a="{"for b,c in pairs(game.players)do local d=game.players[b].get_inventory(defines.inventory.character_main).get_contents()local e=game.players[b].get_quickbar().get_contents()local f={}for g=1,game.players[b].force.character_logistic_slot_count do f[g]=game.players[b].character.get_request_slot(g)end;a=a..b..":{inventory:{"for h,i in pairs(d)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;for h,i in pairs(e)do a=a.."['+"'"+'"..h.."'+"'"+']:"..i..","end;a=a.."},requestSlots:{"for h,i in pairs(f)do a=a.."['+"'"+'"..i["name"].."'+"'"+']:"..i["count"]..", "end;a=a.."}},"end;a=a.."}"game.print(a)
|
||||
local a=game.players["Danielv123"].get_inventory(defines.inventory.character_main).get_contents()local b=game.players["Danielv123"].get_quickbar().get_contents()local c={}for d=1,game.players["Danielv123"].force.character_logistic_slot_count do c[d]=game.players["Danielv123"].character.get_request_slot(d)end;local e="{inventory:{"for f,g in pairs(a)do e=e.."['+"'"+'"..f.."'+"'"+']:"..g..","end;for f,g in pairs(b)do e=e.."['+"'"+'"..f.."'+"'"+']:"..g..","end;e=e.."},requestSlots:{"for f,g in pairs(c)do e=e.."['+"'"+'"..g["name"].."'+"'"+']:"..g["count"]..", "end;e=e.."}}"game.print(e)
|
||||
local inventory = game.players["Danielv123"].get_inventory(defines.inventory.character_main).get_contents()local quickbar = game.players["Danielv123"].get_quickbar().get_contents()local requests = {}for i = 1, game.player.force.character_logistic_slot_count do requests[i] = game.players["Danielv123"].character.get_request_slot(i) end local o = "{inventory:{" for k,v in pairs(inventory) do o = o.."['+"'"+'"..k.."'+"'"+']:"..v.."," end for k,v in pairs(quickbar) do o = o.."['+"'"+'"..k.."'+"'"+']:"..v.."," end o = o.."},requestSlots:{" for k,v in pairs(requests) do o = o.."['+"'"+'"..v["name"].."'+"'"+']:"..v["count"]..", " end o = o.."}}" game.write_file("test.txt", o,true,0)
|
||||
|
||||
|
||||
-- insert items in players inventory and export leftovers
|
||||
|
||||
Whitespace-only changes.
@@ -0,0 +1,100 @@
|
||||
var mkdirp = require("mkdirp-promise");
|
||||
var mkdirpSync = require("mkdirp").sync;
|
||||
var fs = require("fs");
|
||||
var events = require("events");
|
||||
var path = require("path");
|
||||
|
||||
var objectOps = require("./../../lib/objectOps");
|
||||
|
||||
var LinvoDB = require("linvodb3");
|
||||
// LinvoDB.dbPath = "./database/linvodb/";
|
||||
|
||||
|
||||
let allowLogging = true;
|
||||
let log = string => (allowLogging)? console.log(string):""
|
||||
|
||||
module.exports = class chunkStore {
|
||||
constructor(name, chunkSize = 32, path = "./chunkStore/"){
|
||||
if(!name || (typeof name != "string" && typeof name != "number")) throw new Error("chunkstore requires a name string as first param");
|
||||
name = name.toString();
|
||||
this.name = name;
|
||||
this.chunkSize = chunkSize;
|
||||
this.path = (path[path.length] == "/") ? path : path+"/";
|
||||
mkdirpSync(path+name);
|
||||
LinvoDB.dbPath = this.path;
|
||||
|
||||
this.db = new LinvoDB(name, {}, {});
|
||||
|
||||
this.eventEmitter = new events.EventEmitter();
|
||||
}
|
||||
setEntity(x/**{number}*/, y/**{number}*/, newEntity/**{object}*/){
|
||||
return new Promise((resolve, reject) => {
|
||||
x = Math.floor(x);
|
||||
y = Math.floor(y);
|
||||
this.db.find({x, y}, (err, docs) => {
|
||||
if(typeof newEntity == "object"){
|
||||
if(err) reject(err);
|
||||
let entityDoc = {x, y, entity: newEntity};
|
||||
if(docs && docs[0] && docs[0]._id){
|
||||
entityDoc._id = docs[0]._id;
|
||||
}
|
||||
this.db.save(entityDoc, (err,docs) => {
|
||||
if(err) reject(err);
|
||||
resolve(docs);
|
||||
this.eventEmitter.emit("change", docs)
|
||||
});
|
||||
} else {
|
||||
docs.forEach(doc => {
|
||||
doc.remove(()=>{
|
||||
// removed :)
|
||||
});
|
||||
});
|
||||
resolve();
|
||||
this.eventEmitter.emit("change", {x,y});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
getEntity(x, y){
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.find({x:Math.floor(x), y:Math.floor(y)}, (err, docs) => {
|
||||
if(err) reject(err);
|
||||
resolve(docs);
|
||||
});
|
||||
});
|
||||
}
|
||||
onEntityChange(eventhandler/**function*/){
|
||||
if(typeof eventhandler == "function") {
|
||||
this.eventEmitter.on("change", eventhandler);
|
||||
}
|
||||
}
|
||||
getChunk(x, y){
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
/*
|
||||
// make new chunk in case we can't find an existing one
|
||||
let chunk = {
|
||||
dataObject: {},
|
||||
position:{x,y}, // fancy ES6 feature that generates to {x:x, y:y}
|
||||
chunkSize:this.chunkSize
|
||||
};
|
||||
fs.readFile(this.path+this.name+"/"+x+"/"+y, (err, chunkFile) => {
|
||||
|
||||
if(err){
|
||||
log(err)
|
||||
resolve(chunk);
|
||||
} else {
|
||||
if(objectOps.isJSON(chunkFile)) {
|
||||
log("returned from file")
|
||||
let chunk = JSON.parse(chunkFile);
|
||||
resolve(chunk);
|
||||
} else {
|
||||
log("file failed")
|
||||
resolve(chunk);
|
||||
}
|
||||
}
|
||||
}); // add x and y values as well and we are done?
|
||||
*/
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
var assert = require("assert");
|
||||
|
||||
var chunkStore = require("./chunkStore.js");
|
||||
|
||||
describe("remoteMap/chunkStore.js", () => {
|
||||
// define some values for our test
|
||||
const inserterX = 14.5; // rounds to 15
|
||||
const inserterY = -17.5; // rounds to -17, +64 would be 47
|
||||
const inserterName = "inserter";
|
||||
|
||||
let store // make our store elevated
|
||||
it("is a constructor", ()=>{
|
||||
assert.equal(typeof chunkStore, "function");
|
||||
// create a new store and elevate it
|
||||
store = new chunkStore("910239"/* instanceID, use defaults for the rest */, undefined, "./database/unitTests/chunkStore/");
|
||||
assert.equal(store.name, "910239");
|
||||
});
|
||||
it("allows you to store the locations of entities", done => {
|
||||
store.setEntity(inserterX, inserterY, {name:inserterName, otherData:"can be arbitrary"}).then(() => {
|
||||
done();
|
||||
// nothing to do here really
|
||||
});
|
||||
});
|
||||
it("returns entities at positions you query for", done => {
|
||||
setTimeout(()=>{
|
||||
store.getEntity(inserterX, inserterY).then(entities => {
|
||||
entity = entities[0]
|
||||
// console.log(entity)
|
||||
assert.equal(entity.x, 14);
|
||||
assert.equal(entity.y, -18);
|
||||
assert.equal(entity.entity.name, "inserter");
|
||||
assert.equal(entity.entity.otherData, "can be arbitrary");
|
||||
done();
|
||||
});
|
||||
},300);
|
||||
});
|
||||
it("deletes entities when not provided with an object as 3rd arg", done => {
|
||||
store.setEntity(inserterX, inserterY).then(entity => {
|
||||
assert(entity === undefined);
|
||||
setTimeout(()=>{
|
||||
store.getEntity(inserterX, inserterY).then(entities => {
|
||||
// console.log(entities)
|
||||
assert.equal(entities.length, 0, "Why are we getting results, it should be empty?");
|
||||
done();
|
||||
});
|
||||
},300);
|
||||
});
|
||||
});
|
||||
describe("validates input by throwing when it is invalid", ()=>{
|
||||
it("throws if called without a string as the first parameter", ()=>{
|
||||
assert.throws(()=>{
|
||||
let throwingChunkStore = new chunkStore(undefined, undefined, "asdasda");
|
||||
});
|
||||
assert.throws(()=>{
|
||||
let throwingChunkStore = new chunkStore({}, undefined, "asdasdas");
|
||||
});
|
||||
});
|
||||
it("converts numbers to strings automatically if they are provided as the first parameter", ()=>{
|
||||
assert.ok(()=>{
|
||||
let nonThrowingChunkStore = new chunkStore(42, undefined, "./database/unitTests/chunkStore/");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -13,5 +13,4 @@ module.exports = {
|
||||
description: modInfo.description,
|
||||
// We'll send everything in this file to your stdin. Beware.
|
||||
scriptOutputFileSubscription: "remoteMap.txt",
|
||||
masterPlugin: "masterPlugin.js",
|
||||
}
|
||||
@@ -2,8 +2,6 @@
|
||||
--
|
||||
-- Copyright (c) 2017 Florian Jung
|
||||
--
|
||||
-- Modified by Daniel Vestøl
|
||||
--
|
||||
-- Permission is hereby granted, free of charge, to any person obtaining a
|
||||
-- copy of this factorio lua stub and associated
|
||||
-- documentation files (the "Software"), to deal in the Software without
|
||||
@@ -24,13 +22,12 @@
|
||||
-- DEALINGS IN THE SOFTWARE.
|
||||
|
||||
function write_file(data)
|
||||
global.tileQueue = global.tileQueue .. data .. '\n'
|
||||
-- stringsToWriteNextTick = stringsToWriteNextTick .. data .. '\n'
|
||||
stringsToWriteNextTick = stringsToWriteNextTick .. data .. '\n'
|
||||
-- game.write_file("remoteMap.txt", data, true)
|
||||
end
|
||||
|
||||
function dump_cached_writes()
|
||||
-- game.write_file("remoteMap.txt", stringsToWriteNextTick, true, 0)
|
||||
game.write_file("remoteMap.txt", stringsToWriteNextTick, true, 0)
|
||||
end
|
||||
|
||||
function complain(text)
|
||||
@@ -92,7 +89,7 @@ function on_chunk_generated(event)
|
||||
end
|
||||
|
||||
-- writeout_resources(surface, area)
|
||||
-- writeout_objects(surface, area)
|
||||
writeout_objects(surface, area)
|
||||
-- writeout_tiles(surface, area)
|
||||
end
|
||||
function on_some_entity_created(event, entp)
|
||||
@@ -107,7 +104,7 @@ function on_some_entity_created(event, entp)
|
||||
complain("wtf, on_some_entity_created has nil entity")
|
||||
return
|
||||
end
|
||||
local entityData = "name="..ent.name..",x="..ent.position.x..",y="..ent.position.y
|
||||
local entityData = ent.name..","..ent.position.x..","..ent.position.y
|
||||
if ent.supports_direction then
|
||||
entityData = entityData .. ",rot="..ent.direction
|
||||
end
|
||||
@@ -129,7 +126,7 @@ function on_some_entity_deleted(event)
|
||||
--local area = {left_top={x=math.floor(ent.position.x), y=math.floor(ent.position.y)}, right_bottom={x=math.floor(ent.position.x)+1, y=math.floor(ent.position.y)+1}}
|
||||
|
||||
--table.insert(todo_next_tick, function () writeout_objects(surface, area ) end)
|
||||
write_file("name=deleted,x="..ent.position.x..",y="..ent.position.y)
|
||||
write_file("deleted,"..ent.position.x..","..ent.position.y)
|
||||
--complain("on_some_entity_deleted: "..ent.name.." at "..ent.position.x..","..ent.position.y)
|
||||
end
|
||||
|
||||
@@ -146,22 +143,3 @@ script.on_event(defines.events.on_player_mined_entity, on_some_entity_deleted) -
|
||||
script.on_event(defines.events.on_robot_mined_entity, on_some_entity_deleted) --entity
|
||||
script.on_event(defines.events.on_resource_depleted, on_some_entity_deleted) --entity
|
||||
|
||||
script.on_init(function()
|
||||
global.tileQueue = ""
|
||||
end)
|
||||
|
||||
remote.remove_interface("remoteMap")
|
||||
remote.add_interface("remoteMap", {
|
||||
exportTiles = function()
|
||||
rcon.print(global.tileQueue)
|
||||
global.tileQueue = ""
|
||||
end,
|
||||
exportChunk = function(xc, yc, ref)
|
||||
local data = ""
|
||||
local entities = game.surfaces[1].find_entities({{xc*32,yc*32},{xc*32+32,yc*32+32}})
|
||||
for _, ent in pairs(entities) do
|
||||
data = data .. "|name:"..ent.name..",direction:"..ent.direction..",x:"..ent.position.x..",y:"..ent.position.y
|
||||
end
|
||||
rcon.print(data)
|
||||
end
|
||||
})
|
||||
@@ -1,67 +1,130 @@
|
||||
const needle = require("needle");
|
||||
const ioClient = require("socket.io-client");
|
||||
|
||||
const debug = true
|
||||
module.exports = class remoteMap {
|
||||
constructor(mergedConfig, messageInterface, extras){
|
||||
const chunkStore = require("./chunkStore.js");
|
||||
|
||||
const moduleConfig = require("./config"); // not to be confused with clusterio config. This config is private to this plugin.
|
||||
|
||||
class remoteMap {
|
||||
constructor(slaveConfig, messageInterface){
|
||||
this.config = slaveConfig;
|
||||
this.messageInterface = messageInterface;
|
||||
this.config = mergedConfig;
|
||||
this.socket = extras.socket;
|
||||
this.instances = {};
|
||||
|
||||
this.socket.on("hello", () => this.socket.emit("registerMapServer"));
|
||||
|
||||
this.socket.on("createEntity", req => {
|
||||
// req = {name:"", position:{x,y}, ?direction:4}
|
||||
// game.player.surface.create_entity{name="small-ship-wreck", position={game.player.position.x-2, game.player.position.y+1}, direction=..., force=...}
|
||||
this.messageInterface("placing "+JSON.stringify(req));
|
||||
if(req && req.name && typeof req.name == "string" && req.position && !isNaN(Number(req.position.x)) && !isNaN(Number(req.position.y))){
|
||||
// delete any entity in that spot first, just in case to avoid overlaps
|
||||
this.messageInterface('/silent-command local toDelete = game.surfaces[1].find_entities({{'+req.position.x+','+req.position.y+'},{'+(req.position.x+0.5)+','+(req.position.y+0.5)+'}}) for i, entity in pairs(toDelete) do entity.destroy() end');
|
||||
// build the entity
|
||||
let command = '/silent-command game.surfaces[1].create_entity{name="'+req.name+'", position={'+Number(req.position.x)+', '+Number(req.position.y)+'}, force="player"'
|
||||
if(req.direction) command += ', direction='+req.direction;
|
||||
command += '}';
|
||||
this.messageInterface(command); // execute command
|
||||
// initialize chunk database
|
||||
this.chunkMap = new chunkStore(this.config.unique, 64, "./database/chunkStore/");
|
||||
this.chunkMap.onEntityChange(entity => {
|
||||
// this.messageInterface("Entity changed: ")
|
||||
// this.messageInterface(entity);
|
||||
// check valid coordinates (dunno what could go wrong, but can never be too sure)
|
||||
if(entity && entity.x !== undefined && !isNaN(Number(entity.x)) && entity.y !== undefined && !isNaN(Number(entity.y))){
|
||||
this.socket.emit("sendEntity", entity);
|
||||
}
|
||||
})
|
||||
this.socket.on("deleteEntity", req => {
|
||||
// req: {position: {x, y}}
|
||||
this.messageInterface('/silent-command local toDelete = game.surfaces[1].find_entities({{'+req.position.x+','+req.position.y+'},{'+(req.position.x+0.5)+','+(req.position.y+0.5)+'}}) for i, entity in pairs(toDelete) do entity.destroy() end');
|
||||
})
|
||||
this.socket.on("getChunk", async (data, callback) => {
|
||||
callback(await this.messageInterface(`/silent-command remote.call("remoteMap", "exportChunk", ${data.x}, ${data.y})`));
|
||||
})
|
||||
});
|
||||
|
||||
// detect new entities created/deleted on the server and update the master
|
||||
setInterval(async ()=>{
|
||||
let data = await this.messageInterface(`/silent-command remote.call("remoteMap", "exportTiles")`)
|
||||
if(data.length > 1) this.socket.emit("updateEntities", data)
|
||||
}, 2000)
|
||||
}
|
||||
getInstanceName(instanceID){
|
||||
return new Promise((resolve, reject) => {
|
||||
let instance = this.instances[instanceID];
|
||||
if(!instance){
|
||||
needle.get(this.config.masterIP+":"+this.config.masterPort+ '/api/slaves', (err, response) => {
|
||||
if(err || response.statusCode != 200) {
|
||||
console.log("Unable to get JSON master/api/slaves, master might be unaccessible");
|
||||
} else if (response && response.body) {
|
||||
if(Buffer.isBuffer(response.body)) {console.log(response.body.toString("utf-8")); throw new Error();}
|
||||
try {
|
||||
for (let index in response.body)
|
||||
this.instances[index] = response.body[index].instanceName;
|
||||
} catch (e){
|
||||
console.log(e);
|
||||
return null;
|
||||
}
|
||||
instance = this.instances[instanceID]
|
||||
if (!instance) instance = instanceID; //somehow the master doesn't know the instance
|
||||
resolve(instance);
|
||||
// set up websocket communication and handle requests from web interface users (which are going through master)
|
||||
// socket should be a global
|
||||
this.socket = ioClient("http://"+this.config.masterIP+":"+this.config.masterPort);
|
||||
this.socket.on("hello", data => {
|
||||
this.socket.emit("registerSlaveMapper", {instanceID: this.config.unique});
|
||||
setInterval(()=>{
|
||||
this.socket.emit("heartbeat"); // send our heartbeat to prevent being assumed dead
|
||||
},10000);
|
||||
});
|
||||
this.socket.on("getChunk", req => {
|
||||
this.messageInterface("getChunk is not supported!");
|
||||
/*this.chunkMap.getChunk(req.x, req.y).then(chunk => {
|
||||
chunk.requesterID = req.requesterID;
|
||||
this.socket.emit("sendChunk", chunk);
|
||||
});*/
|
||||
});
|
||||
this.socket.on("getEntity", req => {
|
||||
// check for valid coordinates
|
||||
if(req.x !== undefined && !isNaN(Number(req.x)) && req.y !== undefined && !isNaN(Number(req.y))){
|
||||
this.chunkMap.getEntity(req.x, req.y).then(entities => {
|
||||
if(entities && entities.length > 0){
|
||||
entities.forEach(entity => {
|
||||
this.socket.emit("sendEntity", entity);
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve(instance);
|
||||
this.messageInterface("socket.getEntity triggered called with invalid parameters");
|
||||
}
|
||||
});
|
||||
// both placing and deleting entities via the web interface
|
||||
this.socket.on("placeEntity", req => {
|
||||
// game.player.surface.create_entity{name="small-ship-wreck", position={game.player.position.x-2, game.player.position.y+1}, direction=..., force=...}
|
||||
this.messageInterface("placing "+JSON.stringify(req));
|
||||
if(req.name && typeof req.name == "string" && req.position && !isNaN(Number(req.position.x)) && !isNaN(Number(req.position.y))){
|
||||
if(req.name == "deleted"){ // {{'+req.position.x+','+req.position.y+'},{'+req.position.x+','+req.position.y+'}}
|
||||
// delete entity from game world using LUA command
|
||||
this.messageInterface('/silent-command local toDelete = game.surfaces[1].find_entities({{'+req.position.x+','+req.position.y+'},{'+(req.position.x+1)+','+(req.position.y+1)+'}}) for i, entity in pairs(toDelete) do entity.die() end');
|
||||
// this position is now empty, delete whatever was there from DB
|
||||
this.deleteEntity(req.position.x, req.position.y);
|
||||
} else {
|
||||
let command = '/silent-command game.surfaces[1].create_entity{name="'+req.name+'", position={'+Number(req.position.x)+', '+Number(req.position.y)+'}, force="neutral"'
|
||||
if(req.direction) command += ', direction='+req.direction;
|
||||
command += '}';
|
||||
this.messageInterface(command); // execute command
|
||||
|
||||
// add entity to the chunkStore database because create_entity does not trigger any events
|
||||
let name = req.name;
|
||||
let entity = {name};
|
||||
if(typeof req.direction == "string"){
|
||||
entity.rot = req.direction;
|
||||
}
|
||||
this.addEntity(req.position.x, req.position.y, entity);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.messageInterface("/silent-command game.print('"+moduleConfig.name+" version "+moduleConfig.version+" enabled')");
|
||||
}
|
||||
}
|
||||
addEntity(x,y,entity){ // add entity to database
|
||||
this.chunkMap.setEntity(Number(x), Number(y), entity).then(data => {
|
||||
this.messageInterface("Added "+entity.name+" to data "+Number(x)+", "+Number(y));
|
||||
}).catch((err)=>this.messageInterface(err));
|
||||
}
|
||||
deleteEntity(x,y){ // remove entity from database
|
||||
// this position is now empty, delete whatever was there from DB
|
||||
this.chunkMap.setEntity(x, y, "delete this entity").then(() => {
|
||||
this.messageInterface("Deleted entity at "+x+", "+y);
|
||||
}).catch((err)=>this.messageInterface(err));
|
||||
}
|
||||
scriptOutput(data){
|
||||
if(data){
|
||||
this.messageInterface(data);
|
||||
// express-transport-belt,-35.5,-14.5,[...]
|
||||
data = data.split(",");
|
||||
if(data && data[0] == undefined){
|
||||
this.messageInterface("empty: ");
|
||||
this.messageInterface(data);
|
||||
return
|
||||
}
|
||||
let name = data[0];
|
||||
let xPos = data[1];
|
||||
let yPos = data[2];
|
||||
if(xPos && yPos && name){
|
||||
if(name == "deleted"){
|
||||
// this position is now empty, delete whatever was there from DB
|
||||
this.deleteEntity(xPos, yPos);
|
||||
} else {
|
||||
let entity = {name};
|
||||
for(let i = 3; i < data.length; i++){
|
||||
if(data[i].includes("=")){
|
||||
let kv = data[i].split("=");
|
||||
entity[kv[0]] = kv[1];
|
||||
}
|
||||
}
|
||||
// save this entity in the database
|
||||
this.addEntity(xPos, yPos, entity);
|
||||
/* this.chunkMap.setEntity(xPos, yPos, entity).then(data => {
|
||||
this.messageInterface("Added "+name+" to data "+xPos+", "+yPos);
|
||||
}).catch((err)=>this.messageInterface(err)); */
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
module.exports = remoteMap;
|
||||
File renamed without changes.
@@ -2,7 +2,7 @@
|
||||
{
|
||||
"name": "remoteMap",
|
||||
"title": "Clusterio remote map",
|
||||
"version": "3.0.0",
|
||||
"version": "2.2.0",
|
||||
"date": "18-11-2017",
|
||||
"author": "Danielv123",
|
||||
"dependencies": ["base >= 0.15"],
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
const fs = require("fs-extra");
|
||||
const path = require("path");
|
||||
const sanitizer = require('sanitizer');
|
||||
|
||||
const editorSockets = [];
|
||||
|
||||
class masterPlugin {
|
||||
constructor({ config, pluginConfig, pluginPath, socketio, express }) {
|
||||
this.config = config;
|
||||
this.pluginConfig = pluginConfig;
|
||||
this.pluginPath = pluginPath;
|
||||
this.io = socketio;
|
||||
this.app = express;
|
||||
|
||||
this.clients = {};
|
||||
this.slaves = {};
|
||||
|
||||
// initialize token auth module
|
||||
this.authenticate = require("./../../lib/authenticate.js")(config);
|
||||
|
||||
// expose UI elements embedded in the master
|
||||
//this.ui = require("./js/ui.js").ui;
|
||||
|
||||
// handle websocket connections
|
||||
this.io.on("connection", socket => {
|
||||
let instanceID = "unknown";
|
||||
socket.on("registerSlave", data => {
|
||||
if (data.instanceID && !isNaN(Number(data.instanceID))) {
|
||||
instanceID = data.instanceID;
|
||||
this.slaves[instanceID] = socket;
|
||||
}
|
||||
socket.on("updateEntities", data => {
|
||||
console.log(data)
|
||||
if(data){
|
||||
// transform from weirdly deliminated string to entity object
|
||||
let entities = data
|
||||
.split("\n")
|
||||
.filter(str => str.length > 0)
|
||||
.map(ent => ent.replace(/[-]/g, "_")) // replace - with _
|
||||
.map(str => str
|
||||
.split(",")
|
||||
.filter(x => x.length > 0)
|
||||
.map(prop => ({
|
||||
k: prop.split("=")[0],
|
||||
v: prop.split("=")[1]})
|
||||
)
|
||||
)
|
||||
.map(arr => {
|
||||
let obj = {}
|
||||
arr.forEach(property => obj[property.k] = property.v)
|
||||
return obj
|
||||
})
|
||||
console.log(entities)
|
||||
editorSockets.forEach(sock => sock.emit("updateEntity", {entities}))
|
||||
}
|
||||
})
|
||||
});
|
||||
socket.on("registerMapEditor", () => {
|
||||
console.log("Registered mapEditor socket")
|
||||
editorSockets.push(socket);
|
||||
|
||||
setTimeout(()=>socket.emit("createEntity", {entity:{
|
||||
name:"fast_inserter",
|
||||
position: {x:30,y:30},
|
||||
direction: 2,
|
||||
}}),10000)
|
||||
|
||||
socket.on("createEntity", data => {
|
||||
this.slaves[Object.keys(this.slaves)[0]].emit("createEntity", data.entity)
|
||||
})
|
||||
socket.on("deleteEntity", data => {
|
||||
this.slaves[Object.keys(this.slaves)[0]].emit("deleteEntity", data.entity)
|
||||
})
|
||||
socket.on("getChunk", (data, callback) => {
|
||||
this.slaves[Object.keys(this.slaves)[0]].emit("getChunk", data, resp => {
|
||||
// parse response
|
||||
// |name:transport-belt,direction:2,x:99,y:1231|name:assembly-machine,direction:6,x:939,y:88
|
||||
console.log(resp)
|
||||
callback(resp
|
||||
.split("|") // split into entities
|
||||
.map(str => str.replace("\n","")) // remove newlines that happen at the end of the last entity (maybe more as well???)
|
||||
.filter(str => str.length > 0) // remove blank entities
|
||||
.filter(str =>
|
||||
// !str.includes("tree") && // don't send entities that can't be rendered anyways
|
||||
!str.includes("dead") &&
|
||||
// !str.includes("rock") &&
|
||||
!str.includes("crude-oil") &&
|
||||
!str.includes("remnants") &&
|
||||
!str.includes("cliff") &&
|
||||
!str.includes("player") &&
|
||||
!str.includes("ore"))
|
||||
.map(ent => ent.split(",") // split entity into properties
|
||||
.map(prop => [prop.split(":")[0], prop.split(":")[1]]) // split properties into KV pairs
|
||||
)
|
||||
.map(entity => { // turn entities into objects
|
||||
let entityObj = {}
|
||||
entity.forEach(property => entityObj[property[0]] = property[1])
|
||||
return entityObj
|
||||
})
|
||||
.map(entity => {
|
||||
entity.name = entity.name.replace(/[-]/g, "_") // replace - with _ in entity names as thats what factorio.blueprint-editor preffers
|
||||
return entity
|
||||
})
|
||||
.filter(entity => Object.keys(entity).length > 1)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
socket.on("disconnect", function () {
|
||||
let i = editorSockets.indexOf(socket);
|
||||
console.log("editorSocket " + (i + 1) + " disconnected, " + (editorSockets.length - 1) + " left");
|
||||
editorSockets.splice(i, 1);
|
||||
});
|
||||
});
|
||||
socket.on("gameChat", async data => {
|
||||
let chatLine = data.data.replace(/(\r\n\t|\n|\r\t)/gm, "").replace("\r", "");
|
||||
//if (typeof chatLine == "string") this.handleChatLine(chatLine, instanceID);
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
async onExit() {
|
||||
return;
|
||||
}
|
||||
async onLoadFinish({ plugins }) {
|
||||
this.masterPlugins = plugins;
|
||||
}
|
||||
parseData(data, sharedData = {}) {
|
||||
let parsedData = [];
|
||||
data = data.split("|");
|
||||
data.forEach(player => {
|
||||
if (player) {
|
||||
let playerData = {};
|
||||
player = player.split(`~`);
|
||||
player.forEach(kv => {
|
||||
kv = kv.split(":");
|
||||
if (kv[1] === undefined || kv[0] === undefined) {
|
||||
console.log(new Error(`Something is wrong! Key:${kv[0]} Value:${kv[1]}`));
|
||||
} else {
|
||||
playerData[kv[0]] = kv[1].trim();
|
||||
}
|
||||
});
|
||||
for (let k in sharedData) {
|
||||
playerData[k] = sharedData[k];
|
||||
}
|
||||
parsedData.push(playerData);
|
||||
}
|
||||
});
|
||||
return parsedData;
|
||||
}
|
||||
}
|
||||
module.exports = masterPlugin;
|
||||
|
||||
function getDatabaseSync(path) {
|
||||
let db;
|
||||
try {
|
||||
db = JSON.parse(fs.readFileSync(path, "utf8"));
|
||||
} catch (e) {
|
||||
db = {};
|
||||
}
|
||||
return db;
|
||||
}
|
||||
async function saveDatabase(path, database) {
|
||||
if (!path) {
|
||||
throw new Error("No path provided!");
|
||||
} else if (!database) {
|
||||
throw new Error("No database provided!");
|
||||
} else {
|
||||
try {
|
||||
await fs.writeFile(path, JSON.stringify(database, null, 4));
|
||||
} catch (e) {
|
||||
throw new Error("Unable to write to database! " + path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
# Remote Map
|
||||
|
||||
A clusterio plugin for remotely exploring and editing the factorio world
|
||||
|
||||
### Setup
|
||||
|
||||
Make a database for storing entities
|
||||
|
||||
docker run --name some-mariadb -e MYSQL_ROOT_PASSWORD=clusterio-remote-map --restart=unless-stopped -d mariadb:latest
|
||||
|
||||
### Dev setup
|
||||
|
||||
Make a database container and expose the ports
|
||||
|
||||
docker run -p 3306:3306 --name some-mariadb -e MYSQL_ROOT_PASSWORD=clusterio-remote-map -d mariadb:latest
|
||||
@@ -1,9 +1,24 @@
|
||||
/silent-command
|
||||
|
||||
local data = {};
|
||||
local force = game.forces["player"];
|
||||
|
||||
for _, tech in pairs(game.forces["player"].technologies) do
|
||||
table.insert(data, tech.name .. ":" .. tostring(tech.researched) .. ":" .. tostring(tech.level));
|
||||
for _, tech in pairs(force.technologies) do
|
||||
local progress;
|
||||
if tech == force.current_research then
|
||||
progress = force.research_progress
|
||||
else
|
||||
progress = force.get_saved_technology_progress(tech.name);
|
||||
end
|
||||
local infinite = tech.enabled == true and tech.prototype.max_level == 4294967295;
|
||||
local tech_data = {
|
||||
tech.name,
|
||||
tostring(tech.researched),
|
||||
tostring(tech.level),
|
||||
tostring(progress),
|
||||
tostring(infinite),
|
||||
}
|
||||
tech_data = table.concat(tech_data, ':')
|
||||
table.insert(data, tech_data);
|
||||
end
|
||||
|
||||
game.write_file("researchSync.txt", table.concat(data, "\n"), false, 0)
|
||||
@@ -1,16 +1,26 @@
|
||||
/silent-command
|
||||
if game.forces['player'].technologies['{tech_name}'] then
|
||||
if game.forces['player'].technologies['{tech_name}'].researched ~= {tech_researched} then
|
||||
game.forces['player'].technologies['{tech_name}'].researched = {tech_researched}
|
||||
end
|
||||
if game.forces['player'].technologies['{tech_name}'].level ~= {tech_level} then
|
||||
game.forces['player'].technologies['{tech_name}'].level = {tech_level}
|
||||
end
|
||||
|
||||
local function updateTech()
|
||||
script.raise_event(defines.events.on_research_finished, {research=game.forces['player'].technologies['{tech_name}'], by_script=true})
|
||||
game.play_sound({path="utility/research_completed"})
|
||||
if game.forces['player'].technologies['{tech_name}'].researched == false then
|
||||
game.print("Technology {tech_name} synced at level {tech_level}")
|
||||
else
|
||||
game.print("Technology {tech_name} synced")
|
||||
end
|
||||
if {notify} then
|
||||
if 1 == {tech_infinite} then
|
||||
game.print("Infinite technology {tech_name} level {tech_level} unlocked")
|
||||
else
|
||||
game.print("Technology {tech_name} researched")
|
||||
end
|
||||
game.play_sound({path="utility/research_completed"})
|
||||
end
|
||||
end
|
||||
|
||||
if game.forces['player'].technologies['{tech_name}'] then
|
||||
if 1 == {tech_infinite} then
|
||||
if (game.forces['player'].technologies['{tech_name}'].level ~= level) then
|
||||
game.forces['player'].technologies['{tech_name}'].level = {tech_level}
|
||||
updateTech()
|
||||
end
|
||||
else
|
||||
if not game.forces['player'].technologies['{tech_name}'].researched then
|
||||
game.forces['player'].technologies['{tech_name}'].researched = true
|
||||
updateTech()
|
||||
end
|
||||
end
|
||||
end
|
||||
+362
-112
@@ -1,127 +1,377 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path')
|
||||
const needle = require("needle");
|
||||
|
||||
|
||||
function time() {
|
||||
let d = new Date()
|
||||
return `${d.getUTCHours()}:${d.getUTCMinutes()}:${d.getUTCSeconds()}.${d.getUTCMilliseconds()}`
|
||||
}
|
||||
|
||||
class ResearchSync {
|
||||
constructor(slaveConfig, messageInterface, extras = {}){
|
||||
this.config = slaveConfig;
|
||||
this.messageInterface = messageInterface;
|
||||
this.functions = this.loadFunctions();
|
||||
|
||||
this.research = {};
|
||||
this.config = slaveConfig
|
||||
this.messageInterface = messageInterface
|
||||
|
||||
setInterval(() => {
|
||||
this.pollResearch();
|
||||
setTimeout(this.doSync.bind(this), 2000);
|
||||
}, extras.researchSyncPollInterval || 5000);
|
||||
}
|
||||
|
||||
pollResearch() {
|
||||
// this.messageInterface("Polling Research\n")
|
||||
this.messageInterface(this.functions.dumpResearch);
|
||||
}
|
||||
|
||||
doSync() {
|
||||
needle.get(this.config.masterIP + ':' + this.config.masterPort + '/api/slaves', (err, resp, slaveData) => {
|
||||
if (err){
|
||||
this.messageInterface("Unable to post JSON master/api/slaves, master might be unreachable");
|
||||
return false;
|
||||
}
|
||||
if (resp.statusCode !== 200){
|
||||
this.messageInterface("got error when calling slaves", resp.statusCode, resp.body);
|
||||
return;
|
||||
}
|
||||
|
||||
let needResearch = {};
|
||||
|
||||
Object.keys(slaveData).forEach(instanceKey => {
|
||||
if (slaveData[instanceKey].unique === this.config.unique.toString()) {
|
||||
return;
|
||||
}
|
||||
if (!slaveData[instanceKey].hasOwnProperty('meta') || !slaveData[instanceKey].meta.hasOwnProperty('research')) {
|
||||
return;
|
||||
}
|
||||
let researchList = slaveData[instanceKey].meta.research;
|
||||
if(researchList){
|
||||
Object.keys(researchList).forEach(researchName => {
|
||||
if (isNaN(researchList[researchName].researched) || isNaN(researchList[researchName].level)) {
|
||||
return;
|
||||
}
|
||||
if (needResearch.hasOwnProperty(researchName)) {
|
||||
if (needResearch[researchName].researched === 0) {
|
||||
needResearch[researchName].researched = parseInt(researchList[researchName].researched);
|
||||
}
|
||||
if (needResearch[researchName].level < parseInt(researchList[researchName].level)) {
|
||||
needResearch[researchName].level = parseInt(researchList[researchName].level);
|
||||
}
|
||||
}
|
||||
else {
|
||||
needResearch[researchName] = {researched: parseInt(researchList[researchName].researched), level: parseInt(researchList[researchName].level)};
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let difference = this.filterResearchDiff(this.research, needResearch);
|
||||
|
||||
Object.keys(difference).forEach((key) => {
|
||||
if (this.research[key]) {
|
||||
let command = this.functions.enableResearch;
|
||||
while(command.includes("{tech_name}")){
|
||||
command = command.replace("{tech_name}", key);
|
||||
command = command.replace("{tech_researched}", difference[key].researched);
|
||||
command = command.replace("{tech_level}", difference[key].level);
|
||||
}
|
||||
this.messageInterface(command);
|
||||
console.log('Unlocking '+ key + ': ' + (difference[key].researched === 0 ? 'false' : 'true') + ' and level ' + difference[key].level + ', was '+ (this.research[key].researched === 0 ? 'false' : 'true') + ' at level ' + this.research[key].level);
|
||||
this.messageInterface("Unlocking research: " + key + " at research state = " + (difference[key].researched === 0 ? 'false' : 'true') + ' and level ' + difference[key].level);
|
||||
this.research[key] = difference[key];
|
||||
}
|
||||
});
|
||||
|
||||
needle.post(this.config.masterIP + ':' + this.config.masterPort + '/api/editSlaveMeta', {
|
||||
instanceID: this.config.unique,
|
||||
password: this.config.clientPassword,
|
||||
meta: {research: this.research}
|
||||
}, {headers: {'x-access-token': this.config.masterAuthToken}}, function (err, resp) {
|
||||
// success?
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
filterResearchDiff(localResearch, remoteResearch) {
|
||||
let diff = {};
|
||||
Object.keys(localResearch).forEach((key) => {
|
||||
if (remoteResearch.hasOwnProperty(key)) {
|
||||
if ((localResearch[key].researched === 0 && localResearch[key].researched !== remoteResearch[key].researched) || localResearch[key].level < remoteResearch[key].level) {
|
||||
if (!isNaN(remoteResearch[key].researched) && !isNaN(remoteResearch[key].level)) {
|
||||
diff[key] = {researched: remoteResearch[key].researched, level: remoteResearch[key].level};
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return diff;
|
||||
}
|
||||
|
||||
loadFunctions() {
|
||||
return {
|
||||
this.functions = {
|
||||
dumpResearch: this.loadFunc("dumpResearch.lua"),
|
||||
enableResearch: this.loadFunc("enableResearch.lua"),
|
||||
};
|
||||
updateProgress: this.loadFunc("updateProgress.lua")
|
||||
}
|
||||
|
||||
this.log_folder = './logs'
|
||||
if (!fs.existsSync(this.log_folder))
|
||||
fs.mkdirSync(this.log_folder);
|
||||
this.log_file = path.join(this.log_folder, `${this.config.unique}-research.log`)
|
||||
this.get_node_name_and_move_log()
|
||||
|
||||
this.research = {}
|
||||
this.prev_research = {}
|
||||
this.initial_request_own_data(
|
||||
() => this.setup_sync_task(extras)
|
||||
)
|
||||
}
|
||||
|
||||
loadFunc(path) {
|
||||
return fs.readFileSync("sharedPlugins/researchSync/" + path,'utf-8').replace(/\r?\n|\r/g,' ');
|
||||
log(data) {
|
||||
try {
|
||||
console.log(`researchSync: ${data}`)
|
||||
fs.appendFileSync(this.log_file, `${time()}: ${data}\n`);
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
scriptOutput(data){
|
||||
let kv = data.split(":");
|
||||
let name = kv[0];
|
||||
let researched = ('true' !== kv[1]
|
||||
? 0
|
||||
: 1);
|
||||
let level = parseInt(kv[2]);
|
||||
if (!isNaN(level) && !isNaN(researched)) {
|
||||
this.research[name] = {researched: researched, level: level};
|
||||
|
||||
error(data) {
|
||||
try {
|
||||
console.error(`researchSync: ${data}`)
|
||||
fs.appendFileSync(this.log_file, `${time()}: ${data}\n`);
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
get_node_name_and_move_log() {
|
||||
const url = `${this.config.masterIP}:${this.config.masterPort}/api/slaves`
|
||||
needle.get(url, {compressed:true}, (err, res, slaves_data) => {
|
||||
if (err)
|
||||
return this.error(err)
|
||||
|
||||
slaves_data = Object.values(slaves_data)
|
||||
let node_data = slaves_data.find(
|
||||
slave_data => slave_data.unique === this.config.unique.toString()
|
||||
)
|
||||
if (node_data === undefined)
|
||||
return this.error('slave was not found')
|
||||
let node_name = node_data.instanceName
|
||||
let log_file = path.join(this.log_folder, `${node_name}-research.log`)
|
||||
if (fs.existsSync(log_file)) {
|
||||
fs.appendFileSync(log_file, fs.readFileSync(this.log_file));
|
||||
fs.unlinkSync(this.log_file)
|
||||
} else {
|
||||
fs.renameSync(this.log_file, log_file)
|
||||
}
|
||||
this.log_file = log_file
|
||||
})
|
||||
}
|
||||
|
||||
initial_request_own_data(callback) {
|
||||
const url = `${this.config.masterIP}:${this.config.masterPort}/api/getSlaveMeta`
|
||||
const data = {
|
||||
instanceID: this.config.unique,
|
||||
password: this.config.clientPassword,
|
||||
}
|
||||
const options = {
|
||||
headers: {'x-access-token': this.config.masterAuthToken},
|
||||
json: true,
|
||||
compressed: true
|
||||
}
|
||||
needle.post(url, data, options, (err, res, techs) => {
|
||||
if (err) {
|
||||
this.log(`Can't get own slave data:`)
|
||||
this.error(err)
|
||||
return
|
||||
}
|
||||
if (res.statusCode === 404) {
|
||||
this.log('slave is not registered yet. Delaying for 5 secs')
|
||||
setTimeout(
|
||||
() => this.initial_request_own_data(callback),
|
||||
5000
|
||||
)
|
||||
return
|
||||
}
|
||||
if (res.statusCode !== 200) {
|
||||
this.log(`Can't get own slave data:`)
|
||||
this.error(`status code ${res.statusCode}, ${res.body}`)
|
||||
return
|
||||
}
|
||||
if(techs.toString()!="") {
|
||||
techs = JSON.parse(techs)
|
||||
if (typeof techs.research === 'object')
|
||||
this.research = techs.research
|
||||
this.log('techs imported from master')
|
||||
} else {
|
||||
this.log('no techs imported from master, since there are none yet')
|
||||
}
|
||||
callback()
|
||||
})
|
||||
}
|
||||
|
||||
setup_sync_task(extras) {
|
||||
const timeout = extras.researchSyncPollInterval || 5000
|
||||
setInterval(() => this.sync_task(), timeout);
|
||||
}
|
||||
|
||||
sync_task() {
|
||||
this.messageInterface(this.functions.dumpResearch);
|
||||
setTimeout(this.request_cluster_data.bind(this), 2000);
|
||||
}
|
||||
|
||||
request_cluster_data() {
|
||||
const slaves_data_url = `${this.config.masterIP}:${this.config.masterPort}/api/slaves`
|
||||
needle.get(slaves_data_url, {compressed:true}, this.sync_researches.bind(this))
|
||||
}
|
||||
|
||||
sync_researches(err, resp, slaves_data) {
|
||||
if (err) {
|
||||
this.messageInterface("Unable to post JSON master/api/slaves, master might be unreachable");
|
||||
return false;
|
||||
}
|
||||
if (resp.statusCode !== 200) {
|
||||
this.messageInterface("got error when calling slaves", resp.statusCode, resp.body);
|
||||
return;
|
||||
}
|
||||
|
||||
slaves_data = Object.values(slaves_data)
|
||||
slaves_data = slaves_data.filter(
|
||||
slave_data => slave_data.unique !== this.config.unique.toString()
|
||||
&& slave_data.meta && slave_data.meta.research
|
||||
)
|
||||
|
||||
this.clear_contribution_to_researched_techs()
|
||||
let cluster_techs = this.get_cluster_techs(slaves_data)
|
||||
this.recount_cluster_research_progress(slaves_data, cluster_techs)
|
||||
|
||||
let to_research = this.filter_researched_techs(cluster_techs)
|
||||
let to_update_progress = this.filter_updated_techs(cluster_techs, to_research)
|
||||
|
||||
this.research_technologies(to_research)
|
||||
this.update_technologies_progress(to_update_progress)
|
||||
|
||||
this.print_own_contribution()
|
||||
|
||||
needle.post(this.config.masterIP + ':' + this.config.masterPort + '/api/editSlaveMeta', {
|
||||
instanceID: this.config.unique,
|
||||
password: this.config.clientPassword,
|
||||
meta: {research: this.research}
|
||||
}, {headers: {'x-access-token': this.config.masterAuthToken}, json: true, compressed:true}, (err, resp) => {
|
||||
if (err)
|
||||
this.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
clear_contribution_to_researched_techs() {
|
||||
// if between updates tech was researched
|
||||
if (!this.prev_research)
|
||||
return
|
||||
for (let [name, research] of Object.entries(this.research)) {
|
||||
let researched
|
||||
if (research.infinite)
|
||||
researched = this.prev_research[name].level < research.level
|
||||
else
|
||||
researched = this.prev_research[name].researched < research.researched
|
||||
|
||||
if (researched)
|
||||
research.contribution = 0
|
||||
}
|
||||
}
|
||||
|
||||
get_cluster_techs(slavesData) {
|
||||
let cluster_techs = {}
|
||||
for (let slave_data of slavesData) {
|
||||
let node_researches = slave_data.meta.research
|
||||
for (let [name, node_tech] of Object.entries(node_researches)) {
|
||||
if (isNaN(node_tech.researched) || isNaN(node_tech.level) || isNaN(node_tech.infinite))
|
||||
continue
|
||||
|
||||
if (cluster_techs[name]) {
|
||||
if (cluster_techs[name].infinite === 1 && cluster_techs[name].level < node_tech.level) {
|
||||
cluster_techs[name].level = node_tech.level
|
||||
} else if (node_tech.researched > cluster_techs[name].researched) {
|
||||
cluster_techs[name].researched = 1
|
||||
}
|
||||
} else {
|
||||
cluster_techs[name] = node_tech
|
||||
}
|
||||
}
|
||||
}
|
||||
return cluster_techs
|
||||
}
|
||||
|
||||
recount_cluster_research_progress(slaves_data, cluster_researches) {
|
||||
for (let [name, research] of Object.entries(cluster_researches))
|
||||
research.progress = this.research[name].contribution
|
||||
|
||||
for (let slave_data of slaves_data) {
|
||||
for (let [name, research] of Object.entries(slave_data.meta.research)) {
|
||||
if (!cluster_researches[name])
|
||||
continue
|
||||
if (isNaN(cluster_researches[name].progress)
|
||||
|| isNaN(research.contribution)
|
||||
|| isNaN(research.level)
|
||||
|| isNaN(cluster_researches[name].level))
|
||||
continue
|
||||
if (cluster_researches[name].level === research.level)
|
||||
cluster_researches[name].progress += research.contribution
|
||||
}
|
||||
}
|
||||
|
||||
for (let [name, research] of Object.entries(cluster_researches)) {
|
||||
if (research.progress > 1) {
|
||||
research.progress = null
|
||||
research.researched = 1
|
||||
this.research[name].contribution = 0
|
||||
research.contribution = 0
|
||||
if (this.research[name].level >= research.level)
|
||||
research.level = this.research[name].level + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filter_researched_techs(cluster_researches) {
|
||||
let local_researches = this.research
|
||||
let result = {};
|
||||
for (let key in local_researches) {
|
||||
if (!cluster_researches[key])
|
||||
continue
|
||||
if (isNaN(cluster_researches[key].researched) || isNaN(cluster_researches[key].level))
|
||||
continue
|
||||
|
||||
let researched
|
||||
if (local_researches[key].infinite)
|
||||
researched = local_researches[key].level < cluster_researches[key].level
|
||||
else
|
||||
researched = local_researches[key].researched < cluster_researches[key].researched
|
||||
|
||||
if (researched)
|
||||
result[key] = cluster_researches[key]
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
filter_updated_techs(cluster_techs, to_research) {
|
||||
let local_techs = this.research
|
||||
let result = {}
|
||||
for (let name in local_techs) {
|
||||
if (name in to_research || !cluster_techs[name])
|
||||
continue
|
||||
if (isNaN(cluster_techs[name].progress))
|
||||
continue
|
||||
if (local_techs[name].progress < cluster_techs[name].progress)
|
||||
result[name] = cluster_techs[name]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
research_technologies(to_research) {
|
||||
for (let name of Object.keys(to_research))
|
||||
if (!this.research[name])
|
||||
delete to_research[name]
|
||||
|
||||
const notify = Object.keys(to_research).length === 1
|
||||
for (let [name, tech] of Object.entries(to_research)) {
|
||||
this.research[name].contribution = 0
|
||||
this.research[name].progress = null
|
||||
let command = this.functions.enableResearch;
|
||||
command = command.replace(/{tech_name}/g, name);
|
||||
command = command.replace(/{tech_researched}/g, tech.researched);
|
||||
command = command.replace(/{tech_level}/g, tech.level);
|
||||
command = command.replace(/{tech_infinite}/g, tech.infinite);
|
||||
command = command.replace(/{notify}/g, notify);
|
||||
this.messageInterface(command);
|
||||
let log_message = tech.infinite
|
||||
? `Unlocking infinite research ${name} at level ${this.research[name].level}`
|
||||
: `Unlocking research ${name}`
|
||||
this.log(log_message);
|
||||
this.messageInterface(log_message);
|
||||
this.research[name] = tech;
|
||||
}
|
||||
}
|
||||
|
||||
update_technologies_progress(to_update) {
|
||||
for (let [name, tech] of Object.entries(to_update)) {
|
||||
if (!this.research[name])
|
||||
continue
|
||||
let progress = this.research[name].progress
|
||||
if (progress === null)
|
||||
progress = 'nil'
|
||||
let command = this.functions.updateProgress
|
||||
command = command.replace(/{tech_name}/g, name)
|
||||
command = command.replace(/{last_check_progress}/g, progress)
|
||||
command = command.replace(/{new_progress}/g, tech.progress)
|
||||
this.messageInterface(command);
|
||||
this.log(
|
||||
`Updating ${name}: ${this.research[name].progress} += ${tech.progress - this.research[name].progress}`
|
||||
);
|
||||
this.research[name].progress = tech.progress
|
||||
}
|
||||
}
|
||||
|
||||
print_own_contribution() {
|
||||
if (!this.prev_research)
|
||||
return
|
||||
for (let [name, tech] of Object.entries(this.research)) {
|
||||
if (!this.prev_research[name])
|
||||
continue
|
||||
let diff = this.research[name].contribution - this.prev_research[name].contribution
|
||||
if (Math.abs(diff) > Number.EPSILON * 1000)
|
||||
this.log(`Own research ${name}: ${this.research[name].progress} += ${diff}`)
|
||||
}
|
||||
}
|
||||
|
||||
loadFunc(path, silent=true) {
|
||||
let command = fs.readFileSync("sharedPlugins/researchSync/" + path,'utf-8')
|
||||
command = command.replace(/\r?\n|\r/g,' ')
|
||||
command = (silent ? '/silent-command ' : '/c ') + command
|
||||
return command;
|
||||
}
|
||||
scriptOutput(data) {
|
||||
let [name, researched, level, progress, infinite] = data.split(":")
|
||||
researched = +(researched === 'true')
|
||||
infinite = +(infinite === 'true')
|
||||
level = parseInt(level);
|
||||
if (progress === 'nil')
|
||||
progress = null
|
||||
else
|
||||
progress = parseFloat(progress)
|
||||
|
||||
if (isNaN(level) || isNaN(researched))
|
||||
return
|
||||
this.prev_research[name] = this.research[name]
|
||||
if (!this.prev_research[name]) {
|
||||
this.prev_research[name] = {
|
||||
researched: null,
|
||||
level: null,
|
||||
progress: null,
|
||||
contribution: 0,
|
||||
infinite,
|
||||
}
|
||||
}
|
||||
this.research[name] = {
|
||||
researched,
|
||||
level,
|
||||
progress,
|
||||
contribution: this.prev_research[name].contribution,
|
||||
infinite
|
||||
}
|
||||
if (this.prev_research[name].progress && this.research[name].progress) {
|
||||
// this.prev_research[name].progress gets updated to overall cluster progress
|
||||
// therefore contribution should be own research progress change over sync interval
|
||||
let contribution = this.research[name].progress - this.prev_research[name].progress
|
||||
this.research[name].contribution += contribution
|
||||
}
|
||||
if (Math.abs(this.research[name].contribution) < Number.EPSILON * 1000) {
|
||||
// if contribution should be 0 but because of floating-point precision is e.g. 2.2564e-18
|
||||
this.research[name].contribution = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ describe("researchSync/index.js", ()=>{
|
||||
assert.equal(typeof researchSync, "function");
|
||||
});
|
||||
describe("class researchSync()", ()=>{
|
||||
it(".filterResearchDiff(object1, object2) returns research that needs enabling on the local instance", ()=>{
|
||||
it(".filter_researched_techs(object1, object2) returns research that needs enabling on the local instance", ()=>{
|
||||
let reSync = new researchSync({}, function(){});
|
||||
|
||||
let obj1 = {
|
||||
@@ -24,9 +24,9 @@ describe("researchSync/index.js", ()=>{
|
||||
c:{researched: false, level: 0},
|
||||
e:{researched: false, level: 50},
|
||||
};
|
||||
assert(typeof reSync.filterResearchDiff == "function");
|
||||
assert(typeof reSync.filter_researched_techs == "function");
|
||||
|
||||
let diffResult = reSync.filterResearchDiff(obj1, obj2);
|
||||
let diffResult = reSync.filter_researched_techs(obj1, obj2);
|
||||
|
||||
assert(diffResult.a === undefined);
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
|
||||
local function get_tech_progress(force, tech)
|
||||
if tech == force.current_research then
|
||||
return force.research_progress
|
||||
else
|
||||
return force.get_saved_technology_progress(tech.name);
|
||||
end
|
||||
end
|
||||
local function set_tech_progress(force, tech, progress)
|
||||
if tech == force.current_research then
|
||||
force.research_progress = progress
|
||||
else
|
||||
force.set_saved_technology_progress(tech.name, progress);
|
||||
end
|
||||
end
|
||||
|
||||
local force = game.forces['player']
|
||||
local tech_name = '{tech_name}'
|
||||
local tech = force.technologies[tech_name]
|
||||
local last_check_progress = {last_check_progress}
|
||||
local new_progress = {new_progress}
|
||||
|
||||
local progress_delta = get_tech_progress(force, tech)
|
||||
if last_check_progress ~= nil then
|
||||
progress_delta = progress_delta - last_check_progress
|
||||
end
|
||||
if progress_delta ~= nil then
|
||||
new_progress = new_progress + progress_delta
|
||||
end
|
||||
if new_progress > 1 then
|
||||
new_progress = 1
|
||||
end
|
||||
set_tech_progress(force, tech, new_progress)
|
||||
@@ -12,14 +12,6 @@ module.exports = class remoteCommands {
|
||||
messageInterface("Hotpach installation status: "+hotpatchInstallStatus);
|
||||
if(hotpatchInstallStatus){
|
||||
await this.loadScenariosFromFolder("scenarios") // located in project root folder (with sharedMods, sharedPlugins etc)
|
||||
let plugins = new AsyncArray((await fs.readdir(path.join(__dirname, "../"))))
|
||||
plugins.filterAsync(async plugin => {
|
||||
// check if plugin is a folder and if plugin has a scenarios folder
|
||||
|
||||
})
|
||||
for(let g = 0; g < plugins.length; g++){
|
||||
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
@@ -27,7 +19,7 @@ module.exports = class remoteCommands {
|
||||
await fs.ensureDir(scenarioDir)
|
||||
let scenarios = await fs.readdir(scenarioDir);
|
||||
for(let i = 0; i < scenarios.length; i++){
|
||||
messageInterface(`Loading scenario ${scenarios[i]}`)
|
||||
this.messageInterface(`Loading scenario ${scenarios[i]}`)
|
||||
let startTime = Date.now();
|
||||
let scenPath = path.join(scenarioDir, scenarios[i]);
|
||||
let stat = await fs.stat(scenPath);
|
||||
@@ -68,9 +60,9 @@ module.exports = class remoteCommands {
|
||||
if(name != "control") fileImportString += `["${name}"] = '${files[k]}', `;
|
||||
}
|
||||
fileImportString += `\}`;
|
||||
if(files.control) var returnValue = await messageInterface(`/silent-command remote.call('hotpatch', 'update', '${scenarios[i]}', '1.0.0', '${files.control}', ${fileImportString})`);
|
||||
if(returnValue) messageInterface(returnValue);
|
||||
messageInterface(`Loaded scenario ${scenarios[i]} in ${Math.floor(Date.now()-startTime)}ms`);
|
||||
if(files.control) var returnValue = await this.messageInterface(`/silent-command remote.call('hotpatch', 'update', '${scenarios[i]}', '1.0.0', '${files.control}', ${fileImportString})`);
|
||||
if(returnValue) this.messageInterface(returnValue);
|
||||
this.messageInterface(`Loaded scenario ${scenarios[i]} in ${Math.floor(Date.now()-startTime)}ms`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,20 +100,17 @@ function readDirectorySynchronously(directory) {
|
||||
}
|
||||
class AsyncArray extends Array {
|
||||
constructor(arr) {
|
||||
// this.data = arr; // In place of Array subclassing
|
||||
super(arr)
|
||||
this.data = arr; // In place of Array subclassing
|
||||
}
|
||||
|
||||
filterAsync(predicate) {
|
||||
async filterAsync(predicate) {
|
||||
// Take a copy of the array, it might mutate by the time we've finished
|
||||
const data = Array.from(this.data);
|
||||
// Transform all the elements into an array of promises using the predicate
|
||||
// as the promise
|
||||
return Promise.all(data.map((element, index) => predicate(element, index, data)))
|
||||
// Use the result of the promises to call the underlying sync filter function
|
||||
.then(result => {
|
||||
return data.filter((element, index) => {
|
||||
return result[index];
|
||||
});
|
||||
const result = await Promise.all(data.map((element, index) => predicate(element, index, data)));
|
||||
return data.filter((element, index) => {
|
||||
return result[index];
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -20,13 +20,19 @@ module.exports = class remoteCommands {
|
||||
setInterval(async () => {
|
||||
let stats = await this.getStats();
|
||||
stats.forEach(force => {
|
||||
needle.post(`${mergedConfig.masterIP}:${mergedConfig.masterPort}/api/logStats`, {
|
||||
// I have no idea why I need to do JSON.stringify before passing it to needle.post,
|
||||
// but if you do forceData: force.data, it doesn't work...
|
||||
let request = {
|
||||
instanceID: this.config.unique,
|
||||
force: force.forceName,
|
||||
data: force.data,
|
||||
}, needleOptionsWithTokenAuthHeader, (err, resp) => {
|
||||
if(!err && resp.body && resp.body.ok == false) console.log(resp.body);
|
||||
});
|
||||
forceData: JSON.stringify(force.data),
|
||||
};
|
||||
needle.post(`${mergedConfig.masterIP}:${mergedConfig.masterPort}/api/logStats`,
|
||||
request,
|
||||
needleOptionsWithTokenAuthHeader, (err, resp) => {
|
||||
if(!err && resp.body && resp.body.ok == false) console.log("Error calling /api/logStats: " + resp.body);
|
||||
}
|
||||
);
|
||||
});
|
||||
}, 15000);
|
||||
}
|
||||
|
||||
@@ -26,18 +26,22 @@ class masterPlugin {
|
||||
@returns {string} failure
|
||||
*/
|
||||
app.post("/api/logStats", authenticate.middleware, function(req,res) {
|
||||
let data = req.body.forceData;
|
||||
if(typeof req.body.forceData == "string") {
|
||||
data = JSON.parse(req.body.forceData);
|
||||
}
|
||||
// endpointHitCounter.labels(req.route.path).inc();
|
||||
if(typeof req.body == "object"
|
||||
&& req.body.instanceID
|
||||
&& typeof req.body.instanceID == "string"
|
||||
&& req.body.instanceID.length < 100
|
||||
&& req.body.data
|
||||
&& typeof req.body.data == "object"
|
||||
&& !Array.isArray(req.body.data)){
|
||||
&& data
|
||||
&& typeof data == "object"
|
||||
&& !Array.isArray(data)){
|
||||
try{
|
||||
Object.keys(req.body.data).forEach(statisticName => {
|
||||
Object.keys(req.body.data[statisticName]).forEach(direction => {
|
||||
let statHash = req.body.data[statisticName][direction];
|
||||
Object.keys(data).forEach(statisticName => {
|
||||
Object.keys(data[statisticName]).forEach(direction => {
|
||||
let statHash = data[statisticName][direction];
|
||||
Object.keys(statHash).forEach(item => {
|
||||
let value = Number(statHash[item]);
|
||||
if(value) prometheusStatisticsGauge.labels(req.body.instanceID, req.body.force, statisticName, direction, item).set(value);
|
||||
@@ -50,7 +54,12 @@ class masterPlugin {
|
||||
console.log(e)
|
||||
};
|
||||
} else {
|
||||
res.send("failure");
|
||||
console.log(`/api/logStats: Invalid request. instanceID: ${req.body.instanceID}, dataIsArray: `
|
||||
+ `${Array.isArray(data)}, ${typeof req.body.instanceID}, ${typeof data}`);
|
||||
res.send({
|
||||
ok:false,
|
||||
msg:"Invalid request parameters"
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": false,
|
||||
"target": "es6",
|
||||
"noImplicitAny": false,
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"outDir": "dist",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"*": [
|
||||
"node_modules/",
|
||||
"src/types/*",
|
||||
"master.d.ts",
|
||||
"types/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"**/*"
|
||||
]
|
||||
}
|
||||
Vendored
-39
@@ -1,39 +0,0 @@
|
||||
type ClusterioConfig = {
|
||||
factorioDirectory: string,
|
||||
instanceDirectory: string,
|
||||
databaseDirectory: string,
|
||||
publicIP: string,
|
||||
masterIP: string,
|
||||
masterPort: number,
|
||||
sslPort: number,
|
||||
sslCert: string,
|
||||
sslPrivKey: string,
|
||||
masterAuthToken: string,
|
||||
masterAuthSecret: string,
|
||||
username: string,
|
||||
token: string,
|
||||
factorio_version: string,
|
||||
verify_user_identity: boolean,
|
||||
auto_pause: boolean,
|
||||
allow_commands: "admins-only" | "true" | "false",
|
||||
game_password: string,
|
||||
description: string,
|
||||
visibility: {
|
||||
public: boolean,
|
||||
lan: boolean
|
||||
},
|
||||
itemStats: {
|
||||
maxEntries: number,
|
||||
entriesPerSecond: number
|
||||
},
|
||||
logItemTransfers: boolean,
|
||||
disableFairItemDistribution: boolean,
|
||||
uploadModsToMaster: boolean,
|
||||
msBetweenCommands: 10,
|
||||
allowRemoteCommandExecution: boolean,
|
||||
enableCrossServerShout: boolean,
|
||||
mirrorAllChat: boolean,
|
||||
autosaveInterval: 600000,
|
||||
disablePrometheusPushgateway: boolean,
|
||||
disableImportsOfEverythingExceptElectricity: boolean
|
||||
}
|
||||
Vendored
-4
@@ -1,4 +0,0 @@
|
||||
type Mod = {
|
||||
modName: string,
|
||||
hash: string,
|
||||
}
|
||||
Vendored
-9
@@ -1,9 +0,0 @@
|
||||
interface Request {
|
||||
route?: any,
|
||||
body?: requestBody,
|
||||
}
|
||||
interface requestBody extends Body {
|
||||
unique: string,
|
||||
|
||||
}
|
||||
|
||||
Vendored
-15
@@ -1,15 +0,0 @@
|
||||
interface Slave {
|
||||
time?: string,
|
||||
rconPort?: string,
|
||||
rconPassword?: string,
|
||||
serverPort?: string,
|
||||
unique?: string, // this one isn't really optional, but I got tired of fighting with types and rewriting logic in master.ts
|
||||
publicIP?: string,
|
||||
mods?: Mod[],
|
||||
instanceName?: string,
|
||||
playerCount?: string | number,
|
||||
mac?: string,
|
||||
meta?: {
|
||||
[name:string]:any,
|
||||
},
|
||||
}
|
||||
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
type Slaves = {
|
||||
[name: string]: Slave,
|
||||
}
|
||||
Vendored
-6
@@ -1,6 +0,0 @@
|
||||
type item = {
|
||||
name: string,
|
||||
count: number,
|
||||
instanceName?: string,
|
||||
instanceID?: string,
|
||||
}
|
||||
Vendored
-16
@@ -1,16 +0,0 @@
|
||||
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,
|
||||
}
|
||||
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
interface slaveMapper {
|
||||
instanceID: string,
|
||||
}
|
||||
Reference in new issue
Block a user