Compare commits

..
Author SHA1 Message Date
Godmave d819644b01 Merge pull request #186 from Zeroto/master
Fix notifying about the same research being finished
2019-07-30 08:12:21 +02:00
Sander Homan 042230dc2c Fix notifying about the same research being finished
This is just a bandaid to fix a symptom. The underlying cause is still there, but at least it shouldn't spam the players anymore.
2019-07-29 20:31:42 +02:00
Hornwitser a00183e94c Switch to rcon-client fork (#183)
Switch to using fork of rcon-client where the commands and the responses
are not converted to ASCII before being sent over the wire.  Should fix
the issue with Russian text showing up as garbled letters in the chat as
well as the issue with train schedules using >= and <= conditions.

If rcon-client upstream is updated with the utf8 patch this commit can
be reverted with the version the patch was included in.
2019-07-28 14:42:44 +02:00
Arvids Godjuks c95d9f9464 Added "Doomsday" scenario (#184)
* Added doomsday scenario
2019-07-27 22:59:23 +03:00
Danielv123 a1237d8cfd Added CLI script for deleting players from playerManager 2019-07-26 23:55:33 +02:00
Godmave 95d06293f5 Merge pull request #181 from Hornwitser/patch-1
Fix wrong argument order
2019-07-26 18:53:58 +02:00
Hornwitser 2714b09e5d Fix wrong argument order
Fix compression not applying because the order of the callback and
options arguments were swapped in the call to Needle.get for the
request_cluster_data function.
2019-07-26 18:51:16 +02:00
Godmave e733cfde03 Merge pull request #180 from Godmave/master
add compression to research sync
2019-07-25 23:23:57 +02:00
Godmave 008ea139c6 #add compression 2019-07-25 23:22:46 +02:00
psihius 3338c439dd Empty meta fix for querying new slave check 2019-07-25 22:20:34 +02:00
psihius b956438d8d INVALID REQUEST is not a valid json key for a variable, end up being "undefined" 2019-07-25 22:08:33 +02:00
Godmave cdbbdd41c7 Merge pull request #9 from Danielv123/master
sync
2019-07-23 20:34:51 +02:00
Master-Guy e2775adcc3 /api/rconPasswords fix & /api/slaves security fix (#179)
Fix for /api/slaves to not send rconPasswords
Fix for /api/rconPasswords to work properly and send partial information
2019-07-20 15:52:47 +02:00
Master-Guy d909b4aa47 Fix for /issues/177 (#178)
/api/editSlaveMeta: Removed unneeded rcon check
/api/getSlaveMeta: Removed useless password check
/api/checkMod: Added authenticate.middleware
/api/slaves: Renamed to /api/rconPasswords and changed to authenticate.middleware
/api/runCommand: Changed to authenticate.middleware
2019-07-20 14:33:54 +02:00
Master-Guy 49611a2062 /api/slaves POST (optimized) (#176)
* Adds a /api/slaves POST call that requires authentication. It also returns the rconPasswords, unlike the GET call.
2019-07-19 15:37:49 +03:00
AreYouScared ee6ab6ab72 Remove GPS pings in crosschat (#172)
* Remove GPS pings in crosschat

Cross-server gps pings are annoying, lets fix that

* Lets just do it the right way <3

Should be better

* Take 2

Let's try that again
2019-07-19 12:05:03 +02:00
Arvids Godjuks de92ca96b9 Merge pull request #173 from Master-Guy/master
Add "info" field between client and master
2019-07-18 18:15:37 +03:00
Arvids Godjuks 77281d0662 Merge pull request #174 from MrRozdor/log-research-sync
Add logging to researchSync plugin
2019-07-18 16:25:27 +03:00
TechnoStrife 2831c07000 Add logging to researchSync plugin 2019-07-17 23:21:57 +03:00
Master-Guy 6ac8c0711f JSON fix
Small fix for JSON
2019-07-17 19:23:53 +02:00
Master-Guy c9cbc48d95 2/2 Add "info" field between client and master
Adds a "info" field to config.json and allows the client to send the contents of this info field to the master, so it also shows up (i.e. with /api/slaves)
2019-07-17 18:40:01 +02:00
Master-Guy 50370bf858 1/2 Add "info" field between client and master
Adds a "info" field to config.json and allows the client to send the contents of this info field to the master, so it also shows up (i.e. with /api/slaves)
2019-07-17 18:35:02 +02:00
Godmave d1fd4c3565 Merge pull request #171 from MCRcortex/patch-1
added res.sendStatus(200); to stop client errors throwing
2019-07-17 14:07:14 +02:00
MCRcortex 8672afe940 added res.sendStatus(200); to stop client errors throwing
according to the docs
"The methods on the response object (res) in the following table can send a response to the client, and terminate the request-response cycle. If none of these methods are called from a route handler, the client request will be left hanging."
Last sentence is the critical one.
Sending a status code 200 should fix the issue
2019-07-17 22:04:29 +10:00
Godmave adf6763a0d Merge pull request #170 from MrRozdor/fix-researchsync
Fix researchSync that it counts progress only from techs on same level
2019-07-17 07:16:48 +02:00
TechnoStrife d48a824a69 Fix researchSync that it counts progress only from techs on same level 2019-07-17 00:31:15 +03:00
Godmave f28b73cf31 Merge pull request #169 from MrRozdor/spread_researching
Fix for migation to new researchSync version
2019-07-15 21:50:38 +02:00
TechnoStrife fec3cf5ddf Fix for migation to new researchSync version 2019-07-15 22:49:39 +03:00
Godmave c7cfb8ce70 Merge pull request #168 from MrRozdor/spread_researching
Make any node be able to research anything
2019-07-15 21:27:49 +02:00
TechnoStrife 93c210f0c7 Merge branch 'master' into spread_researching
# Conflicts:
#	sharedPlugins/researchSync/dumpResearch.lua
#	sharedPlugins/researchSync/index.js
2019-07-15 22:14:09 +03:00
TechnoStrife 251ec696a3 Fixes && debugging 2019-07-15 18:29:53 +03:00
Godmave 773a2c412e Merge pull request #167 from veladon/pr/3
fix for stats exporter console errors
2019-07-15 15:05:21 +02:00
veladon c0499e7f27 Merge branch 'master' into pr/3 2019-07-15 13:57:27 +01:00
veladon 0a65946bf4 fix for /api/logstats 2019-07-15 13:47:53 +01:00
veladon 233410add7 fix for stats exporter console errors (#166) 2019-07-15 13:49:49 +02:00
veladon fea9fb28ca Merge branch 'master' of https://github.com/veladon/factorioClusterio 2019-07-15 12:46:45 +01:00
veladon e1b510b441 fix for stats exporter console errors 2019-07-15 12:46:38 +01:00
veladon 34301eb381 Merge pull request #2 from Danielv123/master
merge
2019-07-15 12:44:43 +01:00
TechnoStrife 4a0956d174 Fix a bug causing all slaves data gets cleared on master startup
Cherry-picked from fix-slaves-data-cleanup-on-startup
2019-07-15 11:39:11 +03:00
TechnoStrife 3b814eeea4 Fix a bug causing all slaves data gets cleared on master startup (#165) 2019-07-15 10:34:02 +02:00
TechnoStrife 98c6795f8f Make researchSync work for any research on any node 2019-07-15 00:46:21 +03:00
Godmave 21dd80df9b Merge pull request #8 from Danielv123/master
sync
2019-07-14 20:54:46 +02:00
Arvids Godjuks 0b34f07954 Dump only enabled research (#164) 2019-07-14 17:48:20 +02:00
Daniel c978e8e748 Merge pull request #163 from psihius/master
Mass sync does not spam
2019-07-14 15:58:07 +02:00
psihius 71870daaba Made so mass tech sync does not make sounds or messages, only notifies at the end 2019-07-14 15:00:55 +02:00
Daniel 5a60f317ba Merge pull request #162 from psihius/master
Update to research sync plugin
2019-07-14 14:06:24 +02:00
psihius b666a909ab Formatting and logging adjustments 2019-07-14 01:32:34 +02:00
psihius 0afe35b67c Updated research sync to handle infinite science and hopefully stop it from apply same research in a loop annoying everyone 2019-07-14 01:25:48 +02:00
veladon 096ead5bc4 Merge pull request #1 from Danielv123/master
merge
2019-07-13 18:14:51 +01:00
TechnoStrife fa4c10dce2 Code refactor 2019-07-12 18:52:47 +03:00
Daniel Vestøl 1f8ac50950 Better error message for statisticsExporter 2019-07-09 22:50:28 +02:00
Godmave 10d5b0da9b Merge pull request #7 from Danielv123/master
pull updates
2019-07-06 19:57:53 +02:00
Daniel Vestøl 6c32271c56 Fixed rcon timeout in the right place, set to 200s for 1250kB commands 2019-07-06 12:44:31 +02:00
Daniel Vestøl a122f2b09e Increased rcon timeout to 20s, increased maxPending to 5 2019-07-06 00:46:22 +02:00
Daniel Vestøl f35be2f355 Set max pending rcon requests to 1 2019-07-05 23:42:51 +02:00
Daniel Vestøl d66d22a9cd Updated rcon-client 2019-07-05 23:07:25 +02:00
Godmave 7d6a5bb6d2 Merge pull request #6 from Danielv123/master
getting up to date
2019-06-24 18:51:21 +02:00
Daniel bf2e78d530 Merge pull request #159 from veladon/master
Added cluster model js to /lib/
2019-06-10 14:11:04 -07:00
veladon bf4b7fe1b8 Added cluster model js to /lib/ 2019-06-10 21:40:31 +01:00
Daniel 66ba706c56 Use neural net dole divider by default on new installs
See "useNeuralNetDoleDivider": true,

If this causes issues, simply set it to false in config.json. If you find normal dole division to work badly as well, set disableFairItemDistribution to true.
2019-06-02 15:27:50 +02:00
Daniel 79c9ac839e Merge pull request #158 from axapaxa123/master
New NN, added debt system
2019-06-02 06:26:11 -07:00
Daniel 7f59b200b1 Merge pull request #153 from veladon/master
player to character inventory renames and extra info for EACCESS problem
2019-06-02 06:24:23 -07:00
axapaxa123 d719a73328 New NN, added debt system 2019-06-02 15:13:49 +02:00
axapaxa123 616bb9cc8f Merge pull request #1 from Danielv123/master
Merge pull request #152 from axapaxa123/master
2019-06-02 15:04:32 +02:00
veladon 3ae55cbd27 Updated defines.inventory.player_<x> to defines.inventory.character_<x> as per 0.17.35 change to rename character entity type from "player" to "character". 2019-05-13 20:41:15 +01:00
Daniel 7ae9ff6649 Merge pull request #152 from axapaxa123/master
New NN, updated required data
2019-05-13 10:42:53 -07:00
axapaxa123 0b3007d22d New NN, updated required data 2019-05-13 19:22:55 +02:00
veladon bd7a116860 Added extra instructions to readme for the EACCESS common problem 2019-05-12 18:10:44 +01:00
Daniel Vestøl 0c9ed582cc Fixed scenario_loader 2019-04-22 13:45:09 +02:00
Daniel d7bfc2193e Merge pull request #151 from axapaxa123/master
Neural Dole v2
2019-04-12 11:54:12 -07:00
axapaxa123 ce9e1d876a Updated neural network and interface 2019-04-10 01:14:39 +02:00
axapaxa123 86d4f3dba4 Longer accounting period, moved creation of gauge 2019-04-10 01:13:06 +02:00
axapaxa123 0fe5633926 Moved gauge creation, 0 items don't fail requests 2019-04-10 01:11:32 +02:00
axapaxa123 595b4e5ac9 Update master.js 2019-04-03 14:40:42 +02:00
axapaxa123 7d2ac5808f Update dole_nn_base.js 2019-04-03 14:40:11 +02:00
axapaxa123 fddafaec09 Update remove.js 2019-04-03 14:39:29 +02:00
Daniel Vestøl abf4b5d926 Updated freeplay script to 0.17 version 2019-03-31 17:56:34 +02:00
Daniel Vestøl 00dfae8bd1 Fixed another bug in scenarioLoader 2019-03-24 20:34:53 +01:00
Daniel Vestøl 2ef6c93b05 Use old doledivision as default, new NN based dole is optional 2019-03-24 20:18:03 +01:00
Daniel Vestøl 2165acb171 Merge branch 'neuralDoleDivision' 2019-03-24 20:16:32 +01:00
Daniel Vestøl 8357fa23ce Fixed bug in scenarioLoader 2019-03-24 20:14:47 +01:00
Daniel Vestøl 94d434c8ef Initial release of neural net based dole divider 2019-03-23 00:14:23 +01:00
Daniel Vestøl 56cf131c70 Moved most of /api/remove to its own file 2019-03-22 19:29:56 +01:00
Daniel Vestøl a2be560b8b Updated hotpatch 2019-03-13 20:13:03 +01:00
Daniel a3e33f6164 Merge pull request #148 from Danielv123/snyk-fix-9pjp3n
[Snyk] Fix for 7 vulnerable dependencies
2019-03-10 06:23:51 -07:00
Daniel 01e96a4dcb Merge pull request #149 from wchristian/patch-1
independant -> independent
2019-03-06 22:19:20 -08:00
Christian Walde (Mithaldu) d189a82494 independant -> independent 2019-03-07 05:03:45 +01:00
Daniel Vestøl c15709ddc1 0.17 update: Fixed hotpatch and scenario_loader 2019-02-27 11:05:39 +01:00
snyk-bot 3f29a722bb fix: .snyk & package.json to reduce vulnerabilities
The following vulnerabilities are fixed with a Snyk patch:
- https://snyk.io/vuln/npm:hawk:20160119
- https://snyk.io/vuln/npm:http-signature:20150122
- https://snyk.io/vuln/npm:mime:20170907
- https://snyk.io/vuln/npm:minimatch:20160620
- https://snyk.io/vuln/npm:request:20160119
- https://snyk.io/vuln/npm:tunnel-agent:20170305
- https://snyk.io/vuln/npm:uglify-js:20151024
2019-02-25 06:48:23 +00:00
83 changed files with 4451 additions and 1127 deletions

No files matched your search

+3 -3
View File
@@ -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/
+38 -1
View File
@@ -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'
+3
View File
@@ -0,0 +1,3 @@
{
"CurrentProjectSetting": null
}
+8
View File
@@ -0,0 +1,8 @@
{
"ExpandedNodes": [
"",
"\\sharedPlugins"
],
"SelectedNode": "\\.gitignore",
"PreviewInSolutionExplorer": false
}
File diff suppressed because it is too large. Load diff
BIN
View File
Binary file not shown.
+10 -2
View File
@@ -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);
})();
})();
+19
View File
@@ -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
View File
@@ -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
+5 -1
View File
@@ -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.
+11 -6
View File
@@ -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, "");
+2
View File
@@ -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,
+57
View File
@@ -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
+112
View File
@@ -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
+8
View File
@@ -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
+177
View File
@@ -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
+10
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=IApiSlaves.js.map
+3
View File
@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=IClusterioMasterProxy.js.map
+27
View File
@@ -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
+24
View File
@@ -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
+61
View File
@@ -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
+29
View File
@@ -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
+57
View File
@@ -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
+16
View File
@@ -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
+8
View File
@@ -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
+64
View File
@@ -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
+10
View File
@@ -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
+10
View File
@@ -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
+13
View File
@@ -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
+119
View File
@@ -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}
-15
View File
@@ -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
}
-6
View File
@@ -1,6 +0,0 @@
declare module "getFactorioLocale" {
interface callback {
(error: Error, factorioLocale?: any): void
}
export function asObject(factorioDirectory: string, languageCode: string, callback: callback): void
}
-40
View File
@@ -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
}
-14
View File
@@ -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
}
+2
View File
@@ -0,0 +1,2 @@
const fs = require("fs-extra");
const needle = require("needle");
+75 -31
View File
@@ -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', [===[
+5 -5
View File
@@ -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
+68 -66
View File
@@ -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
+114 -112
View File
@@ -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
View File
@@ -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
View File
@@ -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;
})();
}
+11
View File
@@ -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(){
BIN
View File
Binary file not shown.
+7 -18
View File
@@ -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": {
+3
View File
@@ -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");
});
};
+183
View File
@@ -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
}
+149
View File
@@ -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
]]
+251
View File
@@ -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)
+266
View File
@@ -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
+309
View File
@@ -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
+8 -5
View File
@@ -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
View File
Whitespace-only changes.
+100
View File
@@ -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/");
});
});
});
});
-1
View File
@@ -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
})
+118 -55
View File
@@ -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;
+1 -1
View File
@@ -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"],
-175
View File
@@ -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);
}
}
}
-15
View File
@@ -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
+18 -3
View File
@@ -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)
+24 -14
View File
@@ -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
View File
@@ -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
}
}
}
+3 -3
View File
@@ -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)
+10 -21
View File
@@ -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];
});
}
}
+11 -5
View File
@@ -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"
});
}
});
}
-23
View File
@@ -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": [
"**/*"
]
}
-39
View File
@@ -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
}
-4
View File
@@ -1,4 +0,0 @@
type Mod = {
modName: string,
hash: string,
}
-9
View File
@@ -1,9 +0,0 @@
interface Request {
route?: any,
body?: requestBody,
}
interface requestBody extends Body {
unique: string,
}
-15
View File
@@ -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,
},
}
-3
View File
@@ -1,3 +0,0 @@
type Slaves = {
[name: string]: Slave,
}
-6
View File
@@ -1,6 +0,0 @@
type item = {
name: string,
count: number,
instanceName?: string,
instanceID?: string,
}
-16
View File
@@ -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,
}
-3
View File
@@ -1,3 +0,0 @@
interface slaveMapper {
instanceID: string,
}