mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb3bf980d3 | ||
|
|
82141ac9d7 | ||
|
|
b7ce4a18a4 | ||
|
|
303f9d2999 | ||
|
|
d7bfc2193e | ||
|
|
ce9e1d876a | ||
|
|
86d4f3dba4 | ||
|
|
0fe5633926 | ||
|
|
595b4e5ac9 | ||
|
|
7d2ac5808f | ||
|
|
fddafaec09 | ||
|
|
abf4b5d926 | ||
|
|
00dfae8bd1 | ||
|
|
2ef6c93b05 | ||
|
|
2165acb171 | ||
|
|
8357fa23ce | ||
|
|
94d434c8ef | ||
|
|
56cf131c70 | ||
|
|
a2be560b8b | ||
|
|
a3e33f6164 | ||
|
|
01e96a4dcb | ||
|
|
d189a82494 | ||
|
|
c15709ddc1 | ||
|
|
9f3978e0b5 | ||
|
|
3f29a722bb | ||
|
|
103409b0cd | ||
|
|
7e0b131557 | ||
|
|
f9a9722215 | ||
|
|
e731fcb4a9 | ||
|
|
9377eb9544 | ||
|
|
f6afa488d1 | ||
|
|
936600c98b | ||
|
|
78061df0c2 | ||
|
|
0024b84745 | ||
|
|
7352ee3751 | ||
|
|
3a87aa2959 | ||
|
|
df7100ecba | ||
|
|
df1875f24d | ||
|
|
d9afb92ade | ||
|
|
09f476ec40 | ||
|
|
846853813a | ||
|
|
064ee0827a | ||
|
|
d859cb554a | ||
|
|
eaa8d5373c | ||
|
|
6855923e58 | ||
|
|
a8fb5197fd | ||
|
|
abc94a16c1 | ||
|
|
ceaf69810b | ||
|
|
2efb628884 | ||
|
|
d4600c960e | ||
|
|
81fa5abae0 | ||
|
|
dc9d8bf987 | ||
|
|
1788d72f8f | ||
|
|
659b071eb6 | ||
|
|
274c5af9f5 | ||
|
|
8c0a4b1a9e | ||
|
|
3b9dd6dcc8 | ||
|
|
9b6f8cf652 | ||
|
|
ce13ad50e0 | ||
|
|
18c486855d | ||
|
|
12a0ebf8d1 | ||
|
|
081d9a5bea | ||
|
|
1e868b4ff4 | ||
|
|
58b609cbd0 | ||
|
|
2c6b8becd4 | ||
|
|
1676dc80db | ||
|
|
52d35ab955 | ||
|
|
aaf4d8b6f1 | ||
|
|
8766529e5b | ||
|
|
1b518964fc | ||
|
|
c4a2b50c08 | ||
|
|
3f4309fab0 | ||
|
|
2855268e34 | ||
|
|
ffe42c523a | ||
|
|
db20e9c780 | ||
|
|
02a401c6c3 | ||
|
|
94ac9ca624 | ||
|
|
e2566d2d1c | ||
|
|
852311e84c | ||
|
|
810e4a27b5 | ||
|
|
8a84f4087a | ||
|
|
087ba8dabe | ||
|
|
07b9d0a0f5 | ||
|
|
857a6824fc | ||
|
|
765146b559 | ||
|
|
ffa2d0b0fb | ||
|
|
a76e07c07b | ||
|
|
e71c183b48 | ||
|
|
1faa3671e4 | ||
|
|
5630f7f7f6 | ||
|
|
da268d57e7 | ||
|
|
8518cd54c6 | ||
|
|
3a7527f053 | ||
|
|
cac481307e | ||
|
|
9461978a6e | ||
|
|
4fed43bad6 | ||
|
|
f7f2ce6897 | ||
|
|
49124021a8 | ||
|
|
329f721b11 | ||
|
|
8abeb26dce | ||
|
|
958cdfa15a | ||
|
|
d72a45cc66 | ||
|
|
d913d59d72 | ||
|
|
9ce4f064ad | ||
|
|
d9638b58ea | ||
|
|
260f8a0bb5 | ||
|
|
b12f12a91f | ||
|
|
7784fecb12 | ||
|
|
68637d78b8 | ||
|
|
dc52d64c7e | ||
|
|
adbf28d6bd | ||
|
|
42037fc451 | ||
|
|
8e9bcc6a62 | ||
|
|
789e9d37c8 | ||
|
|
5c2eb009e2 | ||
|
|
f91ff41dff | ||
|
|
94b2bb5463 | ||
|
|
002dae9911 | ||
|
|
e73a6052fe | ||
|
|
724248b09b | ||
|
|
3557b2d0ad | ||
|
|
1d5d8abd12 | ||
|
|
98131ee66e | ||
|
|
509a13062e | ||
|
|
02c545c650 | ||
|
|
af42b1fcd3 | ||
|
|
5dcbf45487 | ||
|
|
9501861285 | ||
|
|
f8bf979327 | ||
|
|
6962324a45 | ||
|
|
cf0c3b012a | ||
|
|
ca49564fb7 | ||
|
|
f13d90aa58 | ||
|
|
9721071ffb | ||
|
|
a77aeb9e5b | ||
|
|
dbd9876b8b | ||
|
|
cfa52dbcca | ||
|
|
8f8436a809 | ||
|
|
bf19001055 | ||
|
|
443e5dc286 | ||
|
|
8267dae10e | ||
|
|
d177fb7304 | ||
|
|
7d99162072 | ||
|
|
2a8228c5a9 | ||
|
|
b86d4fa2ab | ||
|
|
b783ccf2cb | ||
|
|
010e5271ea | ||
|
|
447766c752 | ||
|
|
e4c276054b | ||
|
|
b7f76bf451 | ||
|
|
780feb7eb8 | ||
|
|
b98cb9def6 | ||
|
|
1b6b0abaeb | ||
|
|
ea8f819bd5 | ||
|
|
94bbf62c26 | ||
|
|
a9532dd0d3 | ||
|
|
034db54152 | ||
|
|
17b5967b0f | ||
|
|
226e157f69 | ||
|
|
f72ea7357e | ||
|
|
812265d0fd | ||
|
|
da6f96a34b | ||
|
|
88e8ab454f | ||
|
|
e87c702cf2 | ||
|
|
e6c706a83e | ||
|
|
47b74a07d1 | ||
|
|
0ee2c5c2f6 | ||
|
|
e73c5055af | ||
|
|
165cfffecb | ||
|
|
ce6dc3af28 | ||
|
|
1d9b6a26a6 | ||
|
|
db89099b2d | ||
|
|
973f0b0e0a | ||
|
|
0b81fb6351 | ||
|
|
ac55b1c442 | ||
|
|
f07b6160bb | ||
|
|
480e2af03e | ||
|
|
8fe908aa5d | ||
|
|
cf1a97ac9f | ||
|
|
6dd25cb1e8 | ||
|
|
ce24a40d8c | ||
|
|
f5d1847975 | ||
|
|
f580f06064 | ||
|
|
f5e908eb5d | ||
|
|
a6d98db6a9 | ||
|
|
b49a37e438 | ||
|
|
c02bb6d58f | ||
|
|
a0e98ecb00 | ||
|
|
6816db0628 | ||
|
|
be73f2f3a5 | ||
|
|
d07112e85e | ||
|
|
1fede26564 | ||
|
|
c7e2308a33 | ||
|
|
5c064e4609 | ||
|
|
6514462a1d | ||
|
|
1e0c275968 | ||
|
|
968a306c5a | ||
|
|
47430b5d28 | ||
|
|
6e5585c86c | ||
|
|
9c9db7882c | ||
|
|
d6369ba295 | ||
|
|
f4e03899e7 | ||
|
|
0afc3d7e78 | ||
|
|
355e8974a4 | ||
|
|
89ad2baa26 | ||
|
|
57f7dec956 | ||
|
|
5b59abb8e3 | ||
|
|
66b64eae1b | ||
|
|
f15ce3fb23 | ||
|
|
41541b01f0 | ||
|
|
c5b55f27b7 | ||
|
|
a67a1d3d1d | ||
|
|
bcd1ec79cb | ||
|
|
594c994560 | ||
|
|
f10d6bbfb5 | ||
|
|
1184e64467 | ||
|
|
02ab34dbf4 | ||
|
|
fc1f3bd077 | ||
|
|
6a7042c0aa | ||
|
|
2aff63e4a1 | ||
|
|
e38aed08bf | ||
|
|
1bd87c4cc9 | ||
|
|
dc19457471 | ||
|
|
84aaae05c2 | ||
|
|
73ce30ded4 | ||
|
|
1b00fbb328 | ||
|
|
29c38fc4d0 | ||
|
|
2e1f7b7f6d | ||
|
|
61e944b061 | ||
|
|
ce0deef83b | ||
|
|
641f980741 | ||
|
|
ebf2fd2204 | ||
|
|
20e66cbd40 | ||
|
|
a26189947d | ||
|
|
0b0b2bc1a9 |
No files matched your search
+20
@@ -22,6 +22,7 @@ coverage
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
dist/
|
||||
|
||||
node_modules/*
|
||||
!node_modules/_app
|
||||
@@ -39,6 +40,7 @@ mod_cache.json
|
||||
static/pictures/spritesheet.js
|
||||
static/pictures/spritesheet.png
|
||||
static/external
|
||||
saves
|
||||
|
||||
# weird pattern to ignore sharedPlugins except not ignore some core modules
|
||||
!sharedPlugins/
|
||||
@@ -48,3 +50,21 @@ sharedPlugins/*
|
||||
!sharedPlugins/remoteMap/
|
||||
!sharedPlugins/remoteCommands/
|
||||
!sharedPlugins/researchSync/
|
||||
!sharedPlugins/globalChat/
|
||||
!sharedPlugins/clusterioMod/
|
||||
!sharedPlugins/serverManager/
|
||||
sharedPlugins/serverManager/database/*
|
||||
sharedPlugins/serverManager/config.json
|
||||
sharedPlugins/serverManager/secret-api-token.txt
|
||||
!sharedPlugins/scenario_loader/
|
||||
sharedPlugins/scenario_loader/scenarios
|
||||
!sharedPlugins/statisticsExporter/
|
||||
|
||||
# File used for marking plugins as disabled
|
||||
sharedPlugins/*/DISABLED
|
||||
|
||||
\.cache-require-paths\.json
|
||||
|
||||
\.vs/
|
||||
|
||||
bin/
|
||||
@@ -0,0 +1,45 @@
|
||||
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
|
||||
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'
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "node"
|
||||
- "9"
|
||||
before_install:
|
||||
- sudo apt-get -qq update
|
||||
- sudo apt-get install -y curl git tar
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"CurrentProjectSetting": null
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"ExpandedNodes": [
|
||||
"",
|
||||
"\\sharedPlugins"
|
||||
],
|
||||
"SelectedNode": "\\.gitignore",
|
||||
"PreviewInSolutionExplorer": false
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+9
-7
@@ -1,19 +1,21 @@
|
||||
FROM node:latest
|
||||
RUN apt-get update && apt install git -y
|
||||
|
||||
FROM node:9.11.2
|
||||
RUN apt-get update && apt install git curl tar -y
|
||||
RUN mkdir factorioClusterio
|
||||
RUN git clone -b dev https://github.com/Danielv123/factorioClusterio.git && cd factorioClusterio && npm install && curl -o factorio.tar.gz -L https://www.factorio.com/get-download/latest/headless/linux64 && tar -xvzf 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
|
||||
RUN mkdir instances sharedMods sharedPlugins
|
||||
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="$FACTORIOPORT" MODE="$MODE" node $MODE\.js start $INSTANCE
|
||||
CMD RCONPORT="$RCONPORT" FACTORIOPORT="$FACTORIOPORT" MODE="$MODE" node $MODE\.js start $INSTANCE
|
||||
+6
-1
@@ -1,4 +1,4 @@
|
||||
MIT License
|
||||
This product uses the MIT License
|
||||
|
||||
Copyright (c) 2016 Daniel Vestøl
|
||||
|
||||
@@ -19,3 +19,8 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
The icons for angels modded items located in /static/pictures/ are licensed
|
||||
by https://mods.factorio.com/user/Arch666Angel under CC BY-NC-ND 4.0
|
||||
https://creativecommons.org/licenses/by-nc-nd/4.0/
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
<img src="./logo.svg" width="100%" align="right">
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
# factorioClusterio
|
||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2FDanielv123%2FfactorioClusterio?ref=badge_shield)
|
||||
|
||||
| Branch | Coverage | Build |
|
||||
|--------|----------| ----- |
|
||||
| Master | [](https://codecov.io/gh/Danielv123/factorioClusterio) | [](https://travis-ci.org/Danielv123/factorioClusterio) |
|
||||
| Dev | [](https://codecov.io/gh/Danielv123/factorioClusterio) | [](https://travis-ci.org/Danielv123/factorioClusterio) |
|
||||
Discord for development/support/play: https://discord.gg/5XuDkje
|
||||
|
||||
If you want to connect to a clusterio cluster, please reffer to the [client](https://github.com/Danielv123/factorioClusterioClient)
|
||||
### Ways to support me/the project:
|
||||
|
||||
* Contribute with code/documentation. Always nice to move the project forward
|
||||
|
||||
* Support me monetarily on [patreon](https://www.patreon.com/danielv123) or paypal: danielv@live.no
|
||||
|
||||
### Table of contents
|
||||
|
||||
@@ -16,6 +22,10 @@ If you want to connect to a clusterio cluster, please reffer to the [client](htt
|
||||
|
||||
* [Windows setup](#windows-setup)
|
||||
|
||||
* [Optional plugins](#Plugins)
|
||||
|
||||
* [Common problems](#Common-problems)
|
||||
|
||||
* [Command cheatsheet](#cheatsheet)
|
||||
|
||||
## Introduction
|
||||
@@ -32,7 +42,16 @@ Features:
|
||||
|
||||
- Inventory combinator to display item levels in the cluster (and epoch time)
|
||||
|
||||
- Reporting of graphs and UPS on master interface
|
||||
- Reporting of graphs and UPS on master interface (Also has extensive Prometheus reporting)
|
||||
|
||||
Optional extras (see [Plugins](#Plugins))
|
||||
|
||||
- Have your inventory synchronize across servers
|
||||
|
||||
- Teleport trans from the border of one world to te next
|
||||
|
||||
- Show in-game chat in discord
|
||||
|
||||
|
||||
Connection diagram:
|
||||
|
||||
@@ -59,10 +78,9 @@ 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 also handles a few other neat things, such as giving you access to epoch time, transmitting combinator signals between worlds (and any other application who wants to) and
|
||||
creating graphs on the master web interface.
|
||||
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.
|
||||
|
||||
## Ubuntu setup
|
||||
|
||||
@@ -70,26 +88,42 @@ NodeJS does not support EOL ubuntu releases. Make sure you are on the most recen
|
||||
|
||||
Master and all slaves:
|
||||
|
||||
sudo curl -sL https://deb.nodesource.com/setup_9.x | sudo -E bash - \
|
||||
sudo apt install -y nodejs python-dev git wget curl tar build-essential \
|
||||
sudo npm install pm2 -g \
|
||||
git clone https://github.com/Danielv123/factorioClusterio.git \
|
||||
cd factorioClusterio \
|
||||
npm install \
|
||||
curl -o factorio.tar.gz -L https://www.factorio.com/get-download/latest/headless/linux64 \
|
||||
sudo curl -sL https://deb.nodesource.com/setup_9.x | sudo -E bash -
|
||||
sudo apt install -y nodejs python-dev git wget curl tar build-essential
|
||||
sudo npm install pm2 -g
|
||||
git clone https://github.com/Danielv123/factorioClusterio.git
|
||||
cd factorioClusterio
|
||||
curl -o factorio.tar.gz -L https://www.factorio.com/get-download/latest/headless/linux64
|
||||
tar -xf factorio.tar.gz
|
||||
npm install
|
||||
cp config.json.dist config.json
|
||||
node ./lib/npmPostinstall.js
|
||||
|
||||
|
||||
downloads and installs nodejs, pm2, git and clusterio. To specify a version, change "latest" in the link to a version number like 0.14.21.
|
||||
|
||||
Now you need to edit the `config.json` file. If you skip this step nothing will work.
|
||||
Pretty much all the blank fields should be filled in, except on the master where a few can be omitted.
|
||||
|
||||
* You get the `masterAuthToken` from `secret-api-token.txt` in the master install dir after running the master twice.
|
||||
|
||||
* You get your factorio matchmaking token from factorio.com
|
||||
|
||||
* The `masterAuthSecret` should never be touched unless you want to invalidate everyones authentication tokens
|
||||
|
||||
**Master**
|
||||
|
||||
pm2 start master.js --name master
|
||||
|
||||
**Server Host**
|
||||
|
||||
To download the mod for all its non vanilla features and items, (optional)
|
||||
|
||||
node client.js download
|
||||
OR
|
||||
|
||||
node master.js
|
||||
|
||||
**Server Host**
|
||||
|
||||
To download the mod for all its non vanilla features and items, (optional, but very recommended)
|
||||
|
||||
node client.js manage shared mods add clusterio
|
||||
|
||||
To create a new instance (its own save, set of mods and config files)
|
||||
|
||||
@@ -97,18 +131,18 @@ To create a new instance (its own save, set of mods and config files)
|
||||
|
||||
To launch an instance with pm2
|
||||
|
||||
pm2 start --name slave client.js -- start [instancename]
|
||||
pm2 start --name [instancename] client.js -- start [instancename]
|
||||
|
||||
use `nano config.json` to change settings.
|
||||
|
||||
**Ubuntu with Docker**
|
||||
|
||||
Clusterio has limited support for using docker.
|
||||
Clusterio has *very* limited support for using docker.
|
||||
|
||||
sudo docker build -t clusterio --no-cache --force-rm factorioClusterio
|
||||
|
||||
|
||||
sudo docker run --name master -e MODE=master -p 1234:8080 -d -it --restart=unless-stopped danielvestol/clusterio
|
||||
|
||||
|
||||
sudo docker run --name slave -e MODE=client -e INSTANCE=world1 -v /srv/clusterio/instances:/factorioClusterio/instances -p 1235:34167 -it --restart=unless-stopped danielvestol/clusterio
|
||||
|
||||
The -v flag is used to specify the instance directory. Your instances (save files etc) will be stored there.
|
||||
@@ -123,13 +157,9 @@ Server host (Slave) = client.js + factorio server
|
||||
|
||||
Game Client = The people connecting to the server
|
||||
|
||||
All Server Hosts (Slaves) AND Game Clients need to be running the clusterio mod located at [github](https://github.com/Danielv123/factorioClusterioMod) Install it by dropping it into either the sharedMods folder or instances/[instanceName]/instanceMods folder.
|
||||
|
||||
There are no requirements for other mods, they can be ran in any configuration allowed by the base game.
|
||||
|
||||
**Requirements**
|
||||
|
||||
download and install nodeJS 6+ from http://nodejs.org
|
||||
download and install nodeJS 8 or 9 from http://nodejs.org
|
||||
|
||||
download and install git from https://git-scm.com/
|
||||
|
||||
@@ -139,29 +169,40 @@ reboot when you are done, then proceed to the next steps. *reboots matter*
|
||||
|
||||
1. download and run https://puu.sh/toFHl/01eebbb333.bat
|
||||
|
||||
2. You do not *need* to follow the given instructions, but you should
|
||||
2. Copy config.json.dist to config.json
|
||||
|
||||
3. type `node master.js`
|
||||
3. Follow the instructions in the bat file
|
||||
3. Some of the instructions are outdated. If you get stuck somewhere, look at the Ubuntu section.
|
||||
|
||||
4. Run `node client.js manage shared mods add clusterio`
|
||||
|
||||
5. type `node master.js` to start the server
|
||||
|
||||
**Server Host**
|
||||
|
||||
1. download and run https://puu.sh/toFHl/01eebbb333.bat
|
||||
|
||||
2. Follow the instructions given
|
||||
2. Copy config.json.dist to config.json
|
||||
|
||||
3. Type `node client.js start [instancename]` to create a new instance.
|
||||
3. Follow the instructions given.
|
||||
|
||||
3.5 Some of the instructions are outdated. If you get stuck somewhere, look at the Ubuntu section.
|
||||
|
||||
4. Type `node client.js start [instancename]` to create a new instance.
|
||||
|
||||
To connect to a master server running on a remote machine, open config.json with your favourite text editor (notepad++). You can also set it up to use the official server browser.
|
||||
|
||||
Change masterIP to something like 31.152.123.14 (provided by master server owner)
|
||||
Change `masterIP `to something like `31.152.123.14` (provided by master server owner)
|
||||
|
||||
Change masterPort to something like 8080 (provided by master server owner)
|
||||
Change `masterPort` to something like `8080` (provided by master server owner)
|
||||
|
||||
Repeat step 3 for more servers on one machine. You should be able to find its port by looking at the slave section on master:8080 (the web interface)
|
||||
Change `masterAuthToken` to the value found in `secret-api-token.txt` on the master server
|
||||
|
||||
Repeat step 4 for more servers on one machine. You should be able to find its port by looking at the slave section on master:8080 (the web interface)
|
||||
|
||||
**GameClient**
|
||||
|
||||
Fancy game client that does the following steps automatically: [clusterioClient](https://github.com/Danielv123/factorioClusterioClient)
|
||||
Fancy game client that does the following steps automatically, but is really old so be warned: [clusterioClient](https://github.com/Danielv123/factorioClusterioClient)
|
||||
|
||||
1. Download the same version of the mod as the slave is running from [the mod portal](https://mods.factorio.com/mods/Danielv123/clusterio) or [github](https://github.com/Danielv123/factorioClusterioMod
|
||||
|
||||
@@ -169,6 +210,55 @@ Fancy game client that does the following steps automatically: [clusterioClient]
|
||||
|
||||
3. Run factorio and connect to slave as a normal MP game. You will find the port number to connect to at http://[masterAddress]:8080
|
||||
|
||||
## Plugins
|
||||
Here are the known Clusterio plugins in the wild:
|
||||
1. [Player Manager](https://github.com/Danielv123/playerManager) - Adds player management to the Web UI and shared inventory handling (beta)
|
||||
2. [DiscordChat](https://github.com/jakedraddy/ClusterioDiscordChat) - Logs in-game chat/joins/leave messages on all instances to a Discord webhook.
|
||||
3. [TrainTeleports](https://github.com/Godmave/clusterioTrainTeleports) - Allows you to teleport cargotrains between servers.
|
||||
|
||||
## Common problems
|
||||
|
||||
### Cannot find module: `/../../config`
|
||||
|
||||
Copy your config.json.dist to config.json and configure it.
|
||||
|
||||
### EACCESS [...] LISTEN 443
|
||||
|
||||
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.
|
||||
|
||||
### InitializationError: First argument must be an abstract-leveldown compliant store
|
||||
|
||||
do:
|
||||
|
||||
npm install linvodb3@3.25.1
|
||||
|
||||
if that does not suffice do
|
||||
|
||||
npm install levelup@1.3.9
|
||||
|
||||
If it still doesn't work, come over to our support discord linked at the top of this readme.
|
||||
|
||||
### Portforwarding doesn't work on the master server when running under WSL
|
||||
|
||||
If you follow the ubuntu guide on WSL (Windows Subsystem for Linux, Bash on Ubuntu on Windows specifically), you will find that the website works on localhost and on your local ip, but not on the global ip. This is also true when you correctly port-forwarded the correct ports. Even when routing this server through nginx in WSL, the issue persists. Then, on a hunch, I tried to run nginx from windows itself and found that this DID work. It came to me that the only usage difference between the 2 versions of nginx is that I got a Windows Firewall popup.
|
||||
|
||||
TLDR: the tested fix is:
|
||||
|
||||
- open your windows firewall and go to advanced settings
|
||||
|
||||
- click on inbound rules and click on new rule...
|
||||
|
||||
- select port and click next >
|
||||
|
||||
- select TCP and select specific local ports and type in the ports that you want to open (comma separated) and click next > 3 times
|
||||
|
||||
- give the rule a name (like 'web server' or something), give it a description (optionally) and click finish
|
||||
|
||||
### Other fixes for other potential problems:
|
||||
|
||||
Sometimes the install fails. Try `node ./lib/npmPostinstall.js` to complete it.
|
||||
|
||||
|
||||
## Cheatsheet
|
||||
|
||||
**To create a new instance/start it**
|
||||
@@ -194,5 +284,5 @@ npm install
|
||||
|
||||
3. Download the latest version of the factorioClusterioMod from its github repo
|
||||
```
|
||||
node client.js download
|
||||
node client.js manage shared mods add clusterio
|
||||
```
|
||||
Binary file not shown.
@@ -0,0 +1,33 @@
|
||||
const needle = require("needle");
|
||||
const config = require("./config.json");
|
||||
|
||||
const masterIP = `${config.masterIP}:${config.masterPort}`;
|
||||
const token = config.masterAuthToken
|
||||
let commandArray = process.argv;
|
||||
commandArray.shift();
|
||||
commandArray.shift();
|
||||
const needleOptionsWithTokenAuthHeader = {
|
||||
headers: {
|
||||
'x-access-token': token,
|
||||
},
|
||||
};
|
||||
|
||||
let command = commandArray.join(" ");
|
||||
let killBiters = `/silent-command local surface=game.surfaces[1] for key, entity in pairs(surface.find_entities_filtered({force="enemy"})) do entity.destroy() end`;
|
||||
|
||||
(async ()=>{
|
||||
needle.post(masterIP+"/api/runCommand", {
|
||||
broadcast:true,
|
||||
command,
|
||||
}, needleOptionsWithTokenAuthHeader, (err, resp) => {
|
||||
console.log(resp.body)
|
||||
});
|
||||
// setInterval(()=>{
|
||||
// needle.post(masterIP+"/api/runCommand", {
|
||||
// broadcast:true,
|
||||
// command: killBiters,
|
||||
// }, needleOptionsWithTokenAuthHeader, (err, resp) => {
|
||||
// console.log(resp.body)
|
||||
// });
|
||||
// }, 1000*60*5);
|
||||
})();
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"__comment": "Where factorio folder. Should be in the same folder as this config file.",
|
||||
"factorioDirectory": "factorio",
|
||||
|
||||
"__comment1": "IP players should connect to to join your servers, should be portforwarded and stuff",
|
||||
"publicIP": "localhost",
|
||||
|
||||
"__comment2": "IP the master web server runs at, can be ignored on the master system",
|
||||
"masterIP": "localhost",
|
||||
"masterPort": 8080,
|
||||
"masterAuthToken": "",
|
||||
|
||||
"__commentAuthSecret": "This is a long random string generated by keyboard mashing. Please mash you own string for security.",
|
||||
"masterAuthSecret": "anfeåpvunuennuusnvzunsrevunåzeufnewnufasiojfåqwoiuajknwkjfdabewfkjadfnewåfundjkcvnewmfqewrmn123",
|
||||
|
||||
"__comment3": "Your factorio.com username for factorio MP services, also username will be admin",
|
||||
"username": "",
|
||||
"__comment4": "Token from your factorio.com profile page. Required for MP and mod manager services.",
|
||||
"token": "",
|
||||
|
||||
"__comment5": "General user settings",
|
||||
"verify_user_identity": false,
|
||||
"auto_pause": false,
|
||||
"allow_commands": "admins-only",
|
||||
"game_password": "",
|
||||
"description": "This server runs Clusterio",
|
||||
"visibility": {
|
||||
"public": true,
|
||||
"lan": true
|
||||
},
|
||||
"__comment6": "Statistic gathering statistics, may affect server performance (master memory usage especially)",
|
||||
"itemStats": {
|
||||
"maxEntries":60,
|
||||
"entriesPerSecond": 0.01666666667
|
||||
},
|
||||
"logItemTransfers": false,
|
||||
"uploadModsToMaster": true,
|
||||
"allowRemoteCommandExecution": false
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"__comment": "Where factorio folder. Should be in the same folder as this config file.",
|
||||
"factorioDirectory": "./factorio",
|
||||
"instanceDirectory": "./instances",
|
||||
"databaseDirectory": "./database",
|
||||
|
||||
"__comment1": "IP players should connect to to join your servers, should be portforwarded and stuff",
|
||||
"publicIP": "localhost",
|
||||
|
||||
"__comment2": "IP the master web server runs at, can be ignored on the master system",
|
||||
"masterIP": "localhost",
|
||||
|
||||
"__commentMasterPort": "HTTP port for the master to listen to",
|
||||
"masterPort": 8080,
|
||||
|
||||
"__commentsslPort": "HTTPS port for the master to listen to. Set to 0 or an empty string to not listen for HTTPS.",
|
||||
"sslPort": 443,
|
||||
"sslCert": "database/certificates/cert.crt",
|
||||
"sslPrivKey": "database/certificates/cert.key",
|
||||
|
||||
"__commentMasterAuthToken": "Authentication token to connect to the master server. Find it in secret-api-token.txt on the master.",
|
||||
"masterAuthToken": "",
|
||||
|
||||
"__commentAuthSecret": "This is a long random string used for generating authentication tokens. Do not share. Regenerates when secret-api-token.txt is deleted.",
|
||||
"masterAuthSecret": "",
|
||||
|
||||
"__comment3": "Your factorio.com username for factorio MP services, also username will be admin on new instances",
|
||||
"username": "",
|
||||
"__comment4": "Token from your factorio.com profile page. Required for MP and mod manager services.",
|
||||
"token": "",
|
||||
"__comment7": "Used for communication with the mod portal, not needed if you manage mods by hand",
|
||||
"factorio_version": "0.16.41",
|
||||
|
||||
"__comment5": "General user settings",
|
||||
"verify_user_identity": true,
|
||||
"auto_pause": false,
|
||||
"allow_commands": "admins-only",
|
||||
"game_password": "",
|
||||
"description": "This server runs Clusterio",
|
||||
"visibility": {
|
||||
"public": true,
|
||||
"lan": true
|
||||
},
|
||||
"__comment6": "Statistic gathering statistics, may affect server performance (master memory usage especially)",
|
||||
"itemStats": {
|
||||
"maxEntries":60,
|
||||
"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": false,
|
||||
"uploadModsToMaster": true,
|
||||
"msBetweenCommands": 10,
|
||||
"allowRemoteCommandExecution": true,
|
||||
"enableCrossServerShout": true,
|
||||
"mirrorAllChat": false,
|
||||
"__comment7": "Autosave interval for items in the master storage in milliseconds. Defaults to one minute when set to 0 or not set.",
|
||||
"autosaveInterval": 600000,
|
||||
"disablePrometheusPushgateway": false,
|
||||
"__comment8": "This setting only affects the master. It makes every import for things that arent 'electricity' return a maximum of 0. Effectively disables teleport chests.",
|
||||
"disableImportsOfEverythingExceptElectricity": false
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
// const config = require("./../config");
|
||||
|
||||
module.exports = function(config){
|
||||
return {
|
||||
middleware: function (req, res, next) {
|
||||
var token = req.headers['x-access-token'];
|
||||
if(!token) return res.status(401).send({ auth: false, message: 'No token provided.', endpoint: req.route.path});
|
||||
|
||||
jwt.verify(token, config.masterAuthSecret, function(err, decoded) {
|
||||
if(err) return res.status(500).send({ auth: false, message: 'Failed to authenticate token.', endpoint: req.route.path});
|
||||
next();
|
||||
});
|
||||
},
|
||||
check: async function(token){
|
||||
return new Promise((resolve, reject) => {
|
||||
jwt.verify(token, config.masterAuthSecret, function(err, decoded) {
|
||||
if(err){
|
||||
resolve({ok:false, msg:err})
|
||||
} else {
|
||||
resolve({ok:true, msg:"Successfully authenticated"});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,118 @@
|
||||
//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
|
||||
|
||||
//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
|
||||
function Dose(numreq,instore,store_last_tick,dole,carry,prev_req,numreq_total_adj)
|
||||
{
|
||||
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];//5..9
|
||||
var inputs=inp1.concat(inp2);
|
||||
var internal=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];
|
||||
var outputs=[0,0,0];
|
||||
//NN: Please don't touch :), code is generated
|
||||
|
||||
outputs[0]=inputs[2]*3.101320028;
|
||||
outputs[0]=outputs[0]+inputs[7]*0.5471593142;
|
||||
outputs[0]=outputs[0]+inputs[3]*2.691268921;
|
||||
outputs[0]=outputs[0]+outputs[1]*-1.64255929;
|
||||
outputs[0]=outputs[0]+inputs[6]*0.7746122479;
|
||||
outputs[0]=sigmoid0(outputs[0]);
|
||||
outputs[1]=inputs[2]*-0.9946503043;
|
||||
outputs[1]=outputs[1]+inputs[1]*0.2128598392;
|
||||
outputs[1]=sigmoid0(outputs[1]);
|
||||
outputs[2]=sigmoid2(outputs[2]);
|
||||
|
||||
outputs[0]=Number(outputs[0]);
|
||||
if (outputs[0]<0) outputs[0]=0;
|
||||
outputs[0]=outputs[0]*numreq;
|
||||
var res1=Math.round(outputs[0]);
|
||||
if (res1>numreq) res1=numreq;
|
||||
if (res1>instore) res1=instore;
|
||||
if (res1<0) res1=0;
|
||||
carry=sigmoid0(outputs[2]);
|
||||
return [res1,sigmoid0(outputs[1]),carry];
|
||||
}
|
||||
|
||||
//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];
|
||||
var internal=[0,0,0,0,0];
|
||||
var outputs=[0,0];
|
||||
//NN: Please don't touch :), code is generated
|
||||
|
||||
internal[2]=sigmoid0(internal[2]);
|
||||
outputs[0]=internal[2]*0.4049594998;
|
||||
outputs[0]=outputs[0]+1.0*-5.026565075;
|
||||
outputs[0]=outputs[0]+inputs[0]*0.9100194573;
|
||||
outputs[0]=outputs[0]+inputs[1]*-1.063795686;
|
||||
outputs[0]=sigmoid1(outputs[0]);
|
||||
outputs[1]=outputs[0]*1.049500823;
|
||||
outputs[1]=outputs[1]*inputs[0]*3.437163115;
|
||||
outputs[1]=sigmoid0(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}
|
||||
@@ -6,6 +6,8 @@ Module for grabbing factorio locale from a local factorio install
|
||||
const fs = require("fs");
|
||||
const ini = require("ini");
|
||||
|
||||
const cache = {};
|
||||
|
||||
/**
|
||||
Gets raw factorio locale from the .ini file.
|
||||
Not available outside of module through exports.
|
||||
@@ -32,14 +34,20 @@ Gets factorios locale as an object, does not respect mods.
|
||||
*/
|
||||
module.exports.asObject = function getLocaleAsObject(factorioDirectory, languageCode, callback){
|
||||
if(typeof factorioDirectory != "string" || typeof languageCode != "string" || typeof callback != "function"){
|
||||
throw "Error: wrong parameters provided";
|
||||
throw new Error("Error: wrong parameters provided");
|
||||
}
|
||||
|
||||
if(!cache[languageCode]){
|
||||
getLocale(factorioDirectory, languageCode, (err, rawLocale) => {
|
||||
if(err){
|
||||
callback(err);
|
||||
} else {
|
||||
let processedLocale = ini.parse(rawLocale);
|
||||
cache[languageCode] = processedLocale;
|
||||
callback(undefined, processedLocale);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
callback(undefined, cache[languageCode]);
|
||||
}
|
||||
getLocale(factorioDirectory, languageCode, (err, rawLocale) => {
|
||||
if(err){
|
||||
callback(err);
|
||||
} else {
|
||||
let processedLocale = ini.parse(rawLocale)
|
||||
callback(undefined, processedLocale);
|
||||
}
|
||||
});
|
||||
}
|
||||
+13
-10
@@ -1,14 +1,17 @@
|
||||
const fileOps = require("_app/fileOps");
|
||||
// const config = require("./../config");
|
||||
|
||||
module.exports = {
|
||||
sync: function(){
|
||||
let instances;
|
||||
try {
|
||||
instances = fileOps.getDirectoriesSync("./instances/");
|
||||
} catch (e) {
|
||||
// there are no instances created yet, probably missing the folder
|
||||
instances = [];
|
||||
}
|
||||
return instances;
|
||||
module.exports = function(config) {
|
||||
return {
|
||||
sync: function(){
|
||||
let instances;
|
||||
try {
|
||||
instances = fileOps.getDirectoriesSync(config.instanceDirectory);
|
||||
} catch (e) {
|
||||
// there are no instances created yet, probably missing the folder
|
||||
instances = [];
|
||||
}
|
||||
return instances;
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,9 @@ console.log()s each entry in the config
|
||||
@param {string} instance Either undefined, "shared" or the instance to show the config for.
|
||||
*/
|
||||
module.exports.displayConfig = function(instance){
|
||||
console.log(asTable(this.getConfig(instance)));
|
||||
}
|
||||
module.exports.getConfig = async function(instance){
|
||||
if(!instance || instance == "shared"){
|
||||
let configArray = [];
|
||||
Object.keys(config).forEach(key=>{
|
||||
@@ -24,7 +27,7 @@ module.exports.displayConfig = function(instance){
|
||||
} else configArray.push({entry: key, value: config[key]});
|
||||
}
|
||||
});
|
||||
console.log(asTable(configArray));
|
||||
return configArray;
|
||||
}
|
||||
}
|
||||
module.exports.editConfig = function(instance, entry, newValue){
|
||||
@@ -33,16 +36,34 @@ module.exports.editConfig = function(instance, entry, newValue){
|
||||
if(objectOps.isJSON(newValue)){
|
||||
config[entry] = JSON.parse(newValue);
|
||||
console.log("Changed config entry "+entry+" from "+JSON.stringify(config[entry])+" to "+JSON.stringify(newValue));
|
||||
console.log("new config file:");
|
||||
console.log(JSON.stringify(config, null, " "))
|
||||
// console.log("new config file:");
|
||||
// console.log(JSON.stringify(config, null, " "))
|
||||
fs.writeFileSync("config.json", JSON.stringify(config, null, 4));
|
||||
return {
|
||||
ok:true,
|
||||
msg:`Changed config entry ${entry} from Object to ${JSON.stringify(newValue)}`,
|
||||
}
|
||||
} else {
|
||||
config[entry] = newValue;
|
||||
console.log("Changed config entry "+entry+" from "+config[entry]+" to '"+newValue+"'");
|
||||
console.log("new config file:");
|
||||
console.log(JSON.stringify(config, null, " "))
|
||||
// console.log("new config file:");
|
||||
// console.log(JSON.stringify(config, null, " "))
|
||||
fs.writeFileSync("config.json", JSON.stringify(config, null, 4));
|
||||
return {
|
||||
ok:true,
|
||||
msg:`Changed config entry ${entry} from ***** to '${newValue}'`,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
ok:false,
|
||||
msg:`No such config parameter`,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
ok:false,
|
||||
msg:`Editing config for single instances is not yet supported`,
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
const fs = require("fs-extra");
|
||||
const needle = require("needle");
|
||||
+236
-196
@@ -7,15 +7,15 @@ const rightpad = require("right-pad");
|
||||
const asTable = require("as-table").configure({delimiter: ' | '});
|
||||
const moment = require("moment");
|
||||
const syncRequest = require("sync-request");
|
||||
const fs = require("fs");
|
||||
const fs = require("fs-extra");
|
||||
const https = require('follow-redirects').https;
|
||||
const factorioAPI = require("node-factorio-api");
|
||||
const sLog = require("single-line-log").stdout;
|
||||
const query = require('cli-interact').getYesNo;
|
||||
|
||||
const fileOps = require("_app/fileOps");
|
||||
const config = require("./../../config");
|
||||
// const config = require("./../../config");
|
||||
|
||||
const instances = require("./../getInstances.js").sync();
|
||||
|
||||
function ellipse(str, max){
|
||||
return str.length > (max - 3) ? str.substring(0,max-3) + '...' : str;
|
||||
@@ -23,208 +23,248 @@ function ellipse(str, max){
|
||||
// https://stackoverflow.com/questions/15900485/correct-way-to-convert-size-in-bytes-to-kb-mb-gb-in-javascript
|
||||
function formatBytes(a,b){if(0==a)return"0 Bytes";var c=1024,d=b||2,e=["Bytes","KB","MB","GB","TB","PB","EB","ZB","YB"],f=Math.floor(Math.log(a)/Math.log(c));return parseFloat((a/Math.pow(c,f)).toFixed(d))+" "+e[f]};
|
||||
|
||||
/**
|
||||
Lists mods for an instance as a table. Warning, handles printing internally.
|
||||
@param {string} instance - Name of the instance or "shared"
|
||||
|
||||
@example
|
||||
var modManager = require("modManager");
|
||||
modManager.listMods("testInstance");
|
||||
// console.logs table of mods installed on instance
|
||||
modManager.listMods("shared");
|
||||
// console.logs mods located in /sharedMods as a nice table
|
||||
*/
|
||||
module.exports.listMods = function(instance){
|
||||
if(instance && instances.includes(instance)){
|
||||
factorioAPI.init(false, 'instances/'+instance+'/instanceMods', 'instances/'+instance+'/saves', '0.16.15');
|
||||
factorioAPI.authenticate({
|
||||
username: config.username,
|
||||
token: config.token,
|
||||
require_ownership: false
|
||||
}).then(token => {
|
||||
console.log("* Currently installed:");
|
||||
factorioAPI.readModZips().then(list => {
|
||||
console.log(asTable(list.map(mod => {
|
||||
return {
|
||||
name:(mod.title || mod.name),
|
||||
version:mod.version,
|
||||
date:mod.date,
|
||||
factorio_ver:mod.factorio_version,
|
||||
author:mod.author,
|
||||
}
|
||||
})));
|
||||
});
|
||||
// Had issues earlier due to bug in node-factorio-api, see https://github.com/Danacus/node-factorio-api/issues/2
|
||||
// bug is resolved, thanks to @Danacus and @brotherpot
|
||||
factorioAPI.getModsFromSaves().then(list => {
|
||||
console.log("* Mods used in save:");
|
||||
console.log(asTable(list[0].mods))
|
||||
});
|
||||
}).catch(reason => {
|
||||
console.log("Failed to authenticate to factorio.com! Please check your connection and the username+token used for authentication in config.json");
|
||||
});
|
||||
} else {
|
||||
// list shared mods in /sharedMods/
|
||||
factorioAPI.init(false, 'sharedMods/', '', '0.16.15');
|
||||
factorioAPI.authenticate({
|
||||
username: config.username,
|
||||
token: config.token,
|
||||
require_ownership: false
|
||||
}).then(() => {
|
||||
console.log("\r\n")
|
||||
console.log("* Mods shared by all instances:")
|
||||
factorioAPI.getInstalledMods().then(mods => {
|
||||
console.log(asTable(mods));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// get JSON data about GH releases
|
||||
/*let res = syncRequest('GET', 'https://api.github.com/repos/Danielv123/factorioClusterioMod/releases', {"headers":{"User-Agent":"Fuck you for requiring user agents!"}});
|
||||
let url = JSON.parse(res.getBody())[0].assets[0].browser_download_url;
|
||||
let name = JSON.parse(res.getBody())[0].assets[0].name;
|
||||
console.log(JSON.parse(res.getBody()))
|
||||
*/
|
||||
}
|
||||
/**
|
||||
Searches mods.factorio.com for mods matching a search term and prints
|
||||
the results to console as a neat looking table
|
||||
module.exports = function(config){
|
||||
if(!config) config = require("./../../config");
|
||||
const instances = require("./../getInstances.js")(config).sync();
|
||||
return {
|
||||
/**
|
||||
Lists mods for an instance as a table. Warning, handles printing internally.
|
||||
@param {string} instance - Name of the instance or "shared"
|
||||
|
||||
@param {string} searchTerm What to search for
|
||||
*/
|
||||
module.exports.findMods = function(searchTerm){
|
||||
factorioAPI.init(false);
|
||||
factorioAPI.authenticate({
|
||||
username: config.username,
|
||||
token: config.token,
|
||||
require_ownership: false
|
||||
}).then(token => {
|
||||
factorioAPI.searchMods(searchTerm).then(mods => {
|
||||
console.log(asTable(mods.reverse().map(mod => {
|
||||
return {
|
||||
title:ellipse(mod.title, 50),
|
||||
name:ellipse(mod.name, 30),
|
||||
downloads:mod.downloads_count,
|
||||
updated: moment(mod.updated_at).format("DD.MM.YY"),
|
||||
owner:mod.owner,
|
||||
//license: mod.license_name,
|
||||
}
|
||||
})))
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
module.exports.addMod = function(term, instance){
|
||||
if(instance == "shared"){ // modDirectory has to end in /
|
||||
var modDirectory = "sharedMods/";
|
||||
} else {
|
||||
var modDirectory = "instances/"+instance+"/instanceMods/";
|
||||
}
|
||||
if(term && term.length > 0){
|
||||
if(term.includes("mods.factorio.com/mods/")){ // https://mods.factorio.com/mods/Danielv123/ChooChoo
|
||||
// treat it as a link download
|
||||
factorioAPI.init(false, modDirectory, "", "0.16.15");
|
||||
factorioAPI.authenticate({
|
||||
username: config.username,
|
||||
token: config.token,
|
||||
require_ownership: false
|
||||
}).then(() => {
|
||||
factorioAPI.searchMods(term.split("/")[term.split("/").length-1]).then(body => {
|
||||
console.log("Im gonna download: "+body[0].name);
|
||||
factorioAPI.downloadMod(body[0]).then(()=>{
|
||||
console.log("Mod downloaded");
|
||||
@example
|
||||
var modManager = require("modManager")(config);
|
||||
modManager.listMods("testInstance");
|
||||
// console.logs table of mods installed on instance
|
||||
modManager.listMods("shared");
|
||||
// console.logs mods located in /sharedMods as a nice table
|
||||
*/
|
||||
listMods: function(instance){
|
||||
return new Promise((resolve, reject) => {
|
||||
if(instance && instances.includes(instance)){
|
||||
factorioAPI.init(false, config.instanceDirectory+'/'+instance+'/instanceMods', config.instanceDirectory+'/'+instance+'/saves', config.factorio_version);
|
||||
factorioAPI.authenticate({
|
||||
username: config.username,
|
||||
token: config.token,
|
||||
require_ownership: false
|
||||
}).then(token => {
|
||||
console.log("* Currently installed:");
|
||||
factorioAPI.readModZips().then(list => {
|
||||
console.log(asTable(list.map(mod => {
|
||||
return {
|
||||
name:(mod.title || mod.name),
|
||||
version:mod.version,
|
||||
date:mod.date,
|
||||
factorio_ver:mod.factorio_version,
|
||||
author:mod.author,
|
||||
}
|
||||
})));
|
||||
});
|
||||
// Had issues earlier due to bug in node-factorio-api, see https://github.com/Danacus/node-factorio-api/issues/2
|
||||
// bug is resolved, thanks to @Danacus and @brotherpot
|
||||
factorioAPI.getModsFromSaves().then(list => {
|
||||
console.log("* Mods used in save:");
|
||||
console.log(asTable(list[0].mods))
|
||||
});
|
||||
}).catch(reason => {
|
||||
console.log("Failed to authenticate to factorio.com! Please check your connection and the username+token used for authentication in config.json");
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
// list shared mods in /sharedMods/
|
||||
factorioAPI.init(false, 'sharedMods/', '', config.factorio_version);
|
||||
factorioAPI.authenticate({
|
||||
username: config.username,
|
||||
token: config.token,
|
||||
require_ownership: false
|
||||
}).then(() => {
|
||||
console.log("\r\n")
|
||||
console.log("* Mods shared by all instances:")
|
||||
factorioAPI.getInstalledMods().then(mods => {
|
||||
console.log(asTable(mods));
|
||||
resolve();
|
||||
});
|
||||
}).catch(e => {
|
||||
reject(new Error(e));
|
||||
});
|
||||
}
|
||||
|
||||
// get JSON data about GH releases
|
||||
/*let res = syncRequest('GET', 'https://api.github.com/repos/Danielv123/factorioClusterioMod/releases', {"headers":{"User-Agent":"Fuck you for requiring user agents!"}});
|
||||
let url = JSON.parse(res.getBody())[0].assets[0].browser_download_url;
|
||||
let name = JSON.parse(res.getBody())[0].assets[0].name;
|
||||
console.log(JSON.parse(res.getBody()))
|
||||
*/
|
||||
});
|
||||
},
|
||||
/**
|
||||
Searches mods.factorio.com for mods matching a search term and prints
|
||||
the results to console as a neat looking table
|
||||
|
||||
@param {string} searchTerm What to search for
|
||||
*/
|
||||
findMods: function(searchTerm){
|
||||
return new Promise((resolve, reject) => {
|
||||
factorioAPI.init(false);
|
||||
factorioAPI.authenticate({
|
||||
username: config.username,
|
||||
token: config.token,
|
||||
require_ownership: false
|
||||
}).then(token => {
|
||||
factorioAPI.searchMods(searchTerm).then(mods => {
|
||||
let result = asTable(mods.reverse().map(mod => {
|
||||
return {
|
||||
title:ellipse(mod.title, 50),
|
||||
name:ellipse(mod.name, 30),
|
||||
downloads:mod.downloads_count,
|
||||
updated: moment(mod.updated_at).format("DD.MM.YY"),
|
||||
latest_version: mod.latest_release.version,
|
||||
owner:mod.owner,
|
||||
//license: mod.license_name,
|
||||
}
|
||||
}));
|
||||
resolve(result);
|
||||
// console.log(result);
|
||||
});
|
||||
}).catch(e => {
|
||||
if(e.contains("Insufficient information")) console.log("We are missing some parameters required for logging in! Make sure your factorio username and token are specified in config.json.");
|
||||
reject(new Error(e));
|
||||
});
|
||||
});
|
||||
} else if(term.includes("github.com")){ // https://github.com/Danielv123/factorioClusterioMod
|
||||
// treat it as a github releases download
|
||||
let apiURL = "https://api.github.com/repos/" + term.split("/")[3] + "/" + term.split("/")[4] + "/releases"; // https://api.github.com/repos/Danielv123/factorioClusterioMod/releases
|
||||
|
||||
let res = syncRequest('GET', apiURL, {"headers":{"User-Agent":"Fuck you for requiring user agents!"}});
|
||||
let url = JSON.parse(res.getBody())[0].assets[0].browser_download_url;
|
||||
},
|
||||
addMod: function(term, instance){
|
||||
return new Promise((resolve, reject) => {
|
||||
if(instance == "shared"){ // modDirectory has to end in /
|
||||
var modDirectory = "sharedMods/";
|
||||
} else {
|
||||
var modDirectory = config.instanceDirectory+"/"+instance+"/instanceMods/";
|
||||
}
|
||||
if(term && term.length > 0){
|
||||
if(term.includes("mods.factorio.com/mods/")){ // https://mods.factorio.com/mods/Danielv123/ChooChoo
|
||||
// treat it as a link download
|
||||
factorioAPI.init(false, modDirectory, "", config.factorio_version);
|
||||
factorioAPI.authenticate({
|
||||
username: config.username,
|
||||
token: config.token,
|
||||
require_ownership: false
|
||||
}).then(() => {
|
||||
factorioAPI.searchMods(term.split("/")[term.split("/").length-1]).then(body => {
|
||||
console.log("Im gonna download: "+body[0].name + " in version: " + body[0].latest_release.version);
|
||||
factorioAPI.downloadMod( {name: body[0].name, version: body[0].latest_release.version}).then(()=>{
|
||||
console.log("Mod downloaded");
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
} else if(term.includes("github.com")){ // https://github.com/Danielv123/factorioClusterioMod
|
||||
// treat it as a github releases download
|
||||
let apiURL = "https://api.github.com/repos/" + term.split("/")[3] + "/" + term.split("/")[4] + "/releases"; // https://api.github.com/repos/Danielv123/factorioClusterioMod/releases
|
||||
|
||||
let res = syncRequest('GET', apiURL, {"headers":{"User-Agent":"Fuck you for requiring user agents!"}});
|
||||
let url = JSON.parse(res.getBody())[0].assets[0].browser_download_url;
|
||||
let name = JSON.parse(res.getBody())[0].assets[0].name;
|
||||
if(url) {
|
||||
console.log("Downloading " + url);
|
||||
let file = fs.createWriteStream(modDirectory + name);
|
||||
let bytesDownloaded = 0;
|
||||
let downloadStartTime = Date.now();
|
||||
https.get(url, function(response) {
|
||||
let bytesToDownload = response.headers['content-length'];
|
||||
response.pipe(file);
|
||||
response.on("data", chunk => {
|
||||
bytesDownloaded += chunk.length;
|
||||
let downloadTimeElapsed = Date.now() - downloadStartTime;
|
||||
let bytesPerSecond = bytesDownloaded / (downloadTimeElapsed/1000);
|
||||
let timeToComplete = Math.floor((bytesToDownload - bytesDownloaded)/bytesPerSecond);
|
||||
sLog("Downloading "+name+" "+(bytesDownloaded/bytesToDownload*100).toPrecision(3)+"% ("+formatBytes(bytesDownloaded)+" / "+formatBytes(bytesToDownload)+")\r\n"+
|
||||
"about "+timeToComplete+" seconds remaining ("+formatBytes(bytesPerSecond)+"/s)");
|
||||
}).on("end", x => {
|
||||
sLog.clear();
|
||||
console.log(" Downloaded in "+moment.utc(moment.duration(Date.now()-downloadStartTime).as('milliseconds')).format('mm:ss')); // because sLog.clear() doesn't seem to do the job of ending the line properly
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// treat it as a search and download the top result
|
||||
factorioAPI.init(false, modDirectory, "", config.factorio_version);
|
||||
factorioAPI.authenticate({
|
||||
username: config.username,
|
||||
token: config.token,
|
||||
require_ownership: false
|
||||
}).then(() => {
|
||||
factorioAPI.searchMods(term).then(body => {
|
||||
console.log(body)
|
||||
console.log("Im gonna download: "+body[0].name + " in version: " + body[0].latest_release.version);
|
||||
factorioAPI.downloadMod( {name: body[0].name, version: body[0].latest_release.version}).then(()=>{
|
||||
console.log("Mod downloaded");
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
reject(new Error("Invalid search term"));
|
||||
}
|
||||
});
|
||||
},
|
||||
removeMod: function removeMod(term, instance, force = false){
|
||||
return new Promise((resolve, reject) => {
|
||||
if(instance == "shared"){ // modDirectory has to end in /
|
||||
var modDirectory = "sharedMods/";
|
||||
} else {
|
||||
var modDirectory = config.instanceDirectory+"/"+instance+"/instanceMods/";
|
||||
}
|
||||
if(term && typeof term == "string" && term.length > 0){
|
||||
let mods = fs.readdirSync(modDirectory);
|
||||
let modsMatchingFilter = mods.filter(mod => mod.toLowerCase().includes(term.toLowerCase()));
|
||||
if(modsMatchingFilter.length < 1){
|
||||
console.log("No mods found matching search");
|
||||
} else if(modsMatchingFilter.length == 1){
|
||||
console.log("Deleting "+modsMatchingFilter[0]);
|
||||
fs.unlinkSync(modDirectory+modsMatchingFilter[0]);
|
||||
console.log("Deleted.");
|
||||
} else if(modsMatchingFilter.length > 1){
|
||||
console.log("Found multiple mods");
|
||||
modsMatchingFilter.forEach(mod => console.log(mod));
|
||||
if(query("Delete all mods matching filter?")){
|
||||
let modsToDelete = modsMatchingFilter.length;
|
||||
for(let i = 0; i < modsToDelete; i++){
|
||||
fs.unlinkSync(modDirectory+modsMatchingFilter[i]);
|
||||
console.log("Deleted "+modsMatchingFilter[i]);
|
||||
}
|
||||
} else console.log("Did not delete mods.")
|
||||
}
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error("Invalid search term"));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 'https://api.github.com/repos/Danielv123/factorioClusterioMod/releases'
|
||||
/**
|
||||
Download mod from github repo by looking for the first binary download in
|
||||
the latest release, saving it to /sharedMods
|
||||
|
||||
@param {string} url a github repo URL, for example https://api.github.com/repos/Danielv123/factorioClusterioMod/releases. Must link to /releases
|
||||
*/
|
||||
downloadFromGitURL: function(url){
|
||||
console.log("Downloading mod from "+url);
|
||||
// get JSON data about releases
|
||||
let res = syncRequest('GET', url, {"headers":{"User-Agent":"Please don't require setting a user agent"}});
|
||||
let downloadUrl = JSON.parse(res.getBody())[0].assets[0].browser_download_url;
|
||||
let name = JSON.parse(res.getBody())[0].assets[0].name;
|
||||
if(url) {
|
||||
console.log("Downloading " + url);
|
||||
let file = fs.createWriteStream(modDirectory + name);
|
||||
let bytesDownloaded = 0;
|
||||
let downloadStartTime = Date.now();
|
||||
https.get(url, function(response) {
|
||||
let bytesToDownload = response.headers['content-length'];
|
||||
console.log(JSON.parse(res.getBody()))
|
||||
if(downloadUrl) {
|
||||
console.log(downloadUrl);
|
||||
let file = fs.createWriteStream("sharedMods/"+name);
|
||||
https.get(downloadUrl, function(response) {
|
||||
response.pipe(file);
|
||||
response.on("data", chunk => {
|
||||
bytesDownloaded += chunk.length;
|
||||
let downloadTimeElapsed = Date.now() - downloadStartTime;
|
||||
let bytesPerSecond = bytesDownloaded / (downloadTimeElapsed/1000);
|
||||
let timeToComplete = Math.floor((bytesToDownload - bytesDownloaded)/bytesPerSecond);
|
||||
sLog("Downloading "+name+" "+(bytesDownloaded/bytesToDownload*100).toPrecision(3)+"% ("+formatBytes(bytesDownloaded)+" / "+formatBytes(bytesToDownload)+")\r\n"+
|
||||
"about "+timeToComplete+" seconds remaining ("+formatBytes(bytesPerSecond)+"/s)");
|
||||
}).on("end", x => {
|
||||
sLog.clear();
|
||||
console.log(" Downloaded in "+moment.utc(moment.duration(Date.now()-downloadStartTime).as('milliseconds')).format('mm:ss')); // because sLog.clear() doesn't seem to do the job of ending the line properly
|
||||
});
|
||||
console.log("Downloaded "+name);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// treat it as a search and download the top result
|
||||
factorioAPI.init(false, modDirectory, "", "0.16.15");
|
||||
factorioAPI.authenticate({
|
||||
username: config.username,
|
||||
token: config.token,
|
||||
require_ownership: false
|
||||
}).then(() => {
|
||||
factorioAPI.searchMods(term).then(body => {
|
||||
console.log("Im gonna download: "+body[0].name);
|
||||
factorioAPI.downloadMod(body[0]).then(()=>{
|
||||
console.log("Mod downloaded");
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
module.exports.removeMod = function removeMod(term, instance, force = false){
|
||||
if(instance == "shared"){ // modDirectory has to end in /
|
||||
var modDirectory = "sharedMods/";
|
||||
} else {
|
||||
var modDirectory = "instances/"+instance+"/instanceMods/";
|
||||
}
|
||||
if(term && typeof term == "string" && term.length > 0){
|
||||
let mods = fs.readdirSync(modDirectory);
|
||||
let modsMatchingFilter = mods.filter(mod => mod.toLowerCase().includes(term.toLowerCase()));
|
||||
if(modsMatchingFilter.length < 1){
|
||||
console.log("No mods found matching search");
|
||||
} else if(modsMatchingFilter.length == 1){
|
||||
console.log("Deleting "+modsMatchingFilter[0]);
|
||||
fs.unlinkSync(modDirectory+modsMatchingFilter[0]);
|
||||
console.log("Deleted.");
|
||||
} else if(modsMatchingFilter.length > 1){
|
||||
console.log("Found multiple mods, not doing anything");
|
||||
modsMatchingFilter.forEach(mod => console.log(mod));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 'https://api.github.com/repos/Danielv123/factorioClusterioMod/releases'
|
||||
/**
|
||||
Download mod from github repo by looking for the first binary download in
|
||||
the latest release, saving it to /sharedMods
|
||||
|
||||
@param {string} url a github repo URL, for example https://api.github.com/repos/Danielv123/factorioClusterioMod/releases. Must link to /releases
|
||||
*/
|
||||
module.exports.downloadFromGitURL = function(url){
|
||||
console.log("Downloading mod from "+url);
|
||||
// get JSON data about releases
|
||||
let res = syncRequest('GET', url, {"headers":{"User-Agent":"Please don't require setting a user agent"}});
|
||||
let downloadUrl = JSON.parse(res.getBody())[0].assets[0].browser_download_url;
|
||||
let name = JSON.parse(res.getBody())[0].assets[0].name;
|
||||
console.log(JSON.parse(res.getBody()))
|
||||
if(downloadUrl) {
|
||||
console.log(downloadUrl);
|
||||
let file = fs.createWriteStream("sharedMods/"+name);
|
||||
https.get(downloadUrl, function(response) {
|
||||
response.pipe(file);
|
||||
console.log("Downloaded "+name);
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
/*
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
const fs = require("fs-extra");
|
||||
const path = require("path");
|
||||
const git = require("simple-git")("./sharedPlugins");
|
||||
|
||||
|
||||
module.exports = function(config){
|
||||
if(!config) config = require("./../../config");
|
||||
|
||||
return {
|
||||
listPlugins: async function(){
|
||||
let plugins = await this.getPlugins();
|
||||
plugins.forEach(plug => {
|
||||
console.log(`${plug.name}`.padEnd(22).substr(0,22)+` - ${plug.description}`.substr(0,80-22));
|
||||
});
|
||||
},
|
||||
getPlugins: async function(){
|
||||
let directories = await fs.readdir("./sharedPlugins");
|
||||
let returnArray = [];
|
||||
for(let i in directories){
|
||||
let dir = directories[i];
|
||||
let pluginContents = await fs.readdir(path.join("./sharedPlugins", dir));
|
||||
// console.log(pluginContents)
|
||||
if(pluginContents.includes("config.js") || pluginContents.includes("config.json")){
|
||||
try{
|
||||
var pluginConfig = require(path.join("./../../sharedPlugins", dir, "config"));
|
||||
pluginConfig.pluginPath = path.join("./sharedPlugins", dir);
|
||||
}catch(e){
|
||||
console.log(e)
|
||||
}
|
||||
|
||||
// check if plugin is enabled
|
||||
try{
|
||||
await fs.stat(path.join(pluginConfig.pluginPath, "DISABLED"))
|
||||
pluginConfig.enabled = false;
|
||||
}catch(e){
|
||||
pluginConfig.enabled = true;
|
||||
}
|
||||
returnArray.push(pluginConfig);
|
||||
}
|
||||
}
|
||||
return returnArray;
|
||||
},
|
||||
addPlugin: function(path){
|
||||
return new Promise((resolve, reject) => {
|
||||
if(path.includes("git")){ // we are decently sure this is a git repo
|
||||
git.clone(path, data => resolve(data))
|
||||
}
|
||||
});
|
||||
},
|
||||
removePlugin: async function(name){
|
||||
let plugins = await this.getPlugins();
|
||||
for(let i in plugins){
|
||||
let plug = plugins[i];
|
||||
if(plug.name.includes(name)){
|
||||
// console.log(`Removing plugin ${plug.name}`);
|
||||
await fs.remove(plug.pluginPath);
|
||||
return {
|
||||
ok:true,
|
||||
msg:`Deleted plugin ${plug.name}`
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok:false,
|
||||
msg:`Plugin matching "${name}" not found`,
|
||||
}
|
||||
},
|
||||
enablePlugin: async function(name, instance){
|
||||
let plugins = await this.getPlugins();
|
||||
let plugin = plugins[findInArray("name", name, plugins)[0]];
|
||||
if(plugin){
|
||||
let files = await fs.readdir(plugin.pluginPath);
|
||||
if(files.includes("DISABLED")){
|
||||
await fs.unlink(path.join(plugin.pluginPath, "DISABLED"));
|
||||
return {
|
||||
ok:true,
|
||||
msg:`Globally enabled plugin ${name}`,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
ok:true,
|
||||
msg:`Plugin ${name} already enabled`,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
ok:false,
|
||||
msg:`Found no excact match for plugin ${name}`,
|
||||
}
|
||||
}
|
||||
},
|
||||
disablePlugin: async function(name, instance){
|
||||
let plugins = await this.getPlugins();
|
||||
let plugin = plugins[findInArray("name", name, plugins)[0]];
|
||||
if(plugin){
|
||||
await fs.writeFile(path.join(plugin.pluginPath, "DISABLED"), "");
|
||||
return {
|
||||
ok:true,
|
||||
msg:`Plugin ${name} is now disabled on this host`,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
ok:false,
|
||||
msg:`Found no excact match for plugin ${name}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function findInArray(key, value, array){
|
||||
let indexes = [];
|
||||
for(let i in array){
|
||||
if(array[i][key] && array[i][key] === value) indexes.push(i);
|
||||
}
|
||||
return indexes;
|
||||
}
|
||||
+12
-12
@@ -5,7 +5,7 @@ var fs = require('fs');
|
||||
|
||||
var modManager = require("./manager/modManager.js");
|
||||
|
||||
modManager.addMod("clusterio", "shared");
|
||||
// modManager.addMod("clusterio", "shared");
|
||||
|
||||
function makeDirectory(path){
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -61,17 +61,17 @@ var directories = [
|
||||
"instances",
|
||||
];
|
||||
let downloadList = [
|
||||
{
|
||||
url: "http://fbpviewer.trakos.pl/images/spritesheet.json",
|
||||
file: "static/pictures/spritesheet.js",
|
||||
callback: function(file){
|
||||
fs.writeFileSync("static/pictures/spritesheet.js", "export default\n"+fs.readFileSync("static/pictures/spritesheet.js"));
|
||||
},
|
||||
},
|
||||
{
|
||||
url: "http://fbpviewer.trakos.pl/images/spritesheet.png",
|
||||
file: "static/pictures/spritesheet.png",
|
||||
},
|
||||
// {
|
||||
// url: "http://fbpviewer.trakos.pl/images/spritesheet.json",
|
||||
// file: "static/pictures/spritesheet.js",
|
||||
// callback: function(file){
|
||||
// fs.writeFileSync("static/pictures/spritesheet.js", "export default\n"+fs.readFileSync("static/pictures/spritesheet.js"));
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// url: "http://fbpviewer.trakos.pl/images/spritesheet.png",
|
||||
// file: "static/pictures/spritesheet.png",
|
||||
// },
|
||||
{
|
||||
url: "https://code.jquery.com/jquery.js",
|
||||
file: "static/external/jquery.js",
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
--[[
|
||||
|
||||
Copyright 2018 Chrisgbk
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
]]
|
||||
-- MIT License, https://opensource.org/licenses/MIT
|
||||
|
||||
-- Hotpatchable control.lua for scenarios
|
||||
-- Supports multiple simultaneously loaded softmods
|
||||
-- This is a WIP
|
||||
-- Version 1.1 alpha
|
||||
-- probably some performance improvements to be made
|
||||
|
||||
_ENV.debug_settings = {
|
||||
level = 'info',
|
||||
log_to_console_only = false,
|
||||
log_to_RCON = false,
|
||||
log_on_tick = false,
|
||||
}
|
||||
|
||||
local hotpatch_tools = require 'hotpatch.mod-tools'
|
||||
require 'hotpatch.remote-interface'
|
||||
require 'hotpatch.commands'
|
||||
require 'hotpatch.gui'
|
||||
|
||||
-- mod code goes here
|
||||
|
||||
-- This is the Factorio Freeplay scenario
|
||||
-- This code, and the freeplay locale files in the zip file, is the property of Wube
|
||||
-- As this is part of the base mod, the developers have given permission for modders to use/adapt the assets therein, for modding Factorio
|
||||
-- Any other use would require licensing/permission from Wube
|
||||
-- If you don't want to run this, either comment this out before creating your scenario, OR uninstall freeplay at runtime
|
||||
hotpatch_tools.static_mod('freeplay', '1.0.0', [===[
|
||||
local util = require("util")
|
||||
local silo_script = require("silo-script")
|
||||
|
||||
local created_items = function()
|
||||
return
|
||||
{
|
||||
["iron-plate"] = 8,
|
||||
["wood"] = 1,
|
||||
["pistol"] = 1,
|
||||
["firearm-magazine"] = 10,
|
||||
["burner-mining-drill"] = 1,
|
||||
["stone-furnace"] = 1
|
||||
}
|
||||
end
|
||||
|
||||
local respawn_items = function()
|
||||
return
|
||||
{
|
||||
["pistol"] = 1,
|
||||
["firearm-magazine"] = 10
|
||||
}
|
||||
end
|
||||
|
||||
for k,v in pairs(silo_script.get_events()) do
|
||||
script.on_event(k, v)
|
||||
end
|
||||
|
||||
script.on_event(defines.events.on_player_created, function(event)
|
||||
local player = game.players[event.player_index]
|
||||
util.insert_safe(player, global.created_items)
|
||||
|
||||
local r = global.chart_distance or 200
|
||||
player.force.chart(player.surface, {{player.position.x - r, player.position.y - r}, {player.position.x + r, player.position.y + r}})
|
||||
|
||||
if not global.skip_intro then
|
||||
if game.is_multiplayer() then
|
||||
player.print({"msg-intro"})
|
||||
else
|
||||
game.show_message_dialog{text = {"msg-intro"}}
|
||||
end
|
||||
end
|
||||
|
||||
silo_script.on_event(event)
|
||||
end)
|
||||
|
||||
script.on_event(defines.events.on_player_respawned, function(event)
|
||||
local player = game.players[event.player_index]
|
||||
util.insert_safe(player, global.respawn_items)
|
||||
silo_script.on_event(event)
|
||||
end)
|
||||
|
||||
script.on_configuration_changed(function(event)
|
||||
global.created_items = global.created_items or created_items()
|
||||
global.respawn_items = global.respawn_items or respawn_items()
|
||||
silo_script.on_configuration_changed(event)
|
||||
end)
|
||||
|
||||
script.on_load(function()
|
||||
silo_script.on_load()
|
||||
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
|
||||
--[[
|
||||
local files = {}
|
||||
|
||||
files['testfolder.anothertest.test'] = [===[
|
||||
script.on_event(defines.events.on_player_changed_position, function(e) game.print('position changed') end)
|
||||
]===]
|
||||
|
||||
files['testfolder.test'] = [===[
|
||||
require 'anothertest.test'
|
||||
]===]
|
||||
|
||||
files['test'] = [===[
|
||||
require 'testfolder.test'
|
||||
]===]
|
||||
|
||||
hotpatch_tools.static_mod('require-test', '1.0.0', [===[
|
||||
require 'test'
|
||||
]===], files)
|
||||
--]]
|
||||
|
||||
-- end of mod code
|
||||
@@ -0,0 +1,135 @@
|
||||
local hotpatch_tools = require 'hotpatch.mod-tools'
|
||||
hotpatch_tools.static_mod('hotpatch-commands', '1.0.1', [===[
|
||||
--[[
|
||||
|
||||
Copyright 2018 Chrisgbk
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
]]
|
||||
-- MIT License, https://opensource.org/licenses/MIT
|
||||
|
||||
local hotpatch_tools = require 'hotpatch.mod-tools'
|
||||
|
||||
--load private API
|
||||
-- mod installation/uninstallation support functions
|
||||
-- These take a mod NAME as a first argument
|
||||
local install_mod = hotpatch_tools.install_mod
|
||||
local find_installed_mod = hotpatch_tools.find_installed_mod
|
||||
local install_mod_file = hotpatch_tools.install_mod_file
|
||||
local uninstall_mod = hotpatch_tools.uninstall_mod
|
||||
|
||||
-- mod interaction functions
|
||||
-- These take a LOADED INDEX as a first argument, except load_mod, which takes an INSTALLED INDEX
|
||||
local load_mod = hotpatch_tools.load_mod
|
||||
local find_loaded_mod = hotpatch_tools.find_loaded_mod
|
||||
local run_mod = hotpatch_tools.run_mod
|
||||
local reset_mod = hotpatch_tools.reset_mod
|
||||
local reset_mod_events = hotpatch_tools.reset_mod_events
|
||||
local register_mod_events = hotpatch_tools.register_mod_events
|
||||
local unload_mod = hotpatch_tools.unload_mod
|
||||
|
||||
-- internal callbacks when a mod registers events
|
||||
local register_event = hotpatch_tools.register_event
|
||||
local register_nth_tick = hotpatch_tools.register_nth_tick
|
||||
local register_on_tick = hotpatch_tools.register_on_tick
|
||||
|
||||
-- mod bootstrap functions
|
||||
-- These take a LOADED INDEX as a first argument
|
||||
local mod_on_init = hotpatch_tools.mod_on_init
|
||||
local mod_on_load = hotpatch_tools.mod_on_load
|
||||
local mod_on_configuration_changed = hotpatch_tools.mod_on_configuration_changed
|
||||
|
||||
local static_mods = hotpatch_tools.static_mods
|
||||
local console = hotpatch_tools.console
|
||||
local debug_log = hotpatch_tools.debug_log
|
||||
local loaded_mods = hotpatch_tools.loaded_mods
|
||||
local installed_mods = hotpatch_tools.installed_mods
|
||||
|
||||
local help_commands = {
|
||||
help = '/hotpatch help\nYou got this far, you probably know all you need to know',
|
||||
list = '/hotpatch list [type]\nLists \'installed\', \'loaded\' or \'running\' mods',
|
||||
}
|
||||
|
||||
local sub_commands
|
||||
sub_commands = {
|
||||
help = function(player_index, param)
|
||||
local caller = (player_index and game.players[player_index]) or console
|
||||
if (not param) or (param == '') then
|
||||
caller.print('Usage: /hotpatch [command] [parameters]')
|
||||
caller.print('Available commands:')
|
||||
for k, _ in pairs(sub_commands) do caller.print(k) end
|
||||
caller.print('Use /hotpatch help [command] for help on a particular command')
|
||||
else
|
||||
local text = help_commands[param]
|
||||
if text then
|
||||
caller.print(text)
|
||||
end
|
||||
end
|
||||
end,
|
||||
list = function(player_index, param)
|
||||
local caller = (player_index and game.players[player_index]) or console
|
||||
if not param or (param == '') then
|
||||
caller.print('Installed mods:')
|
||||
for k, v in pairs(installed_mods) do
|
||||
caller.print(table.concat{'[', k, '] ', v.name, ' ', v.version})
|
||||
end
|
||||
caller.print('Loaded/Running mods:')
|
||||
for k, v in pairs(loaded_mods) do
|
||||
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,
|
||||
}
|
||||
|
||||
local admin_commands = {
|
||||
reinstall = function(player_index, param)
|
||||
local caller = (player_index and game.players[player_index]) or console
|
||||
local mod
|
||||
local installed_index
|
||||
local loaded_index
|
||||
for i = 1, #static_mods do
|
||||
mod = static_mods[i]
|
||||
if mod.name:match('^hotpatch%-.*$') then
|
||||
loaded_index = find_loaded_mod(mod.name)
|
||||
if loaded_index then
|
||||
unload_mod(loaded_index)
|
||||
end
|
||||
install_mod(mod.name, mod.version, mod.code, mod.files)
|
||||
installed_index = find_installed_mod(mod.name)
|
||||
load_mod(installed_index)
|
||||
loaded_index = find_loaded_mod(mod.name)
|
||||
run_mod(loaded_index)
|
||||
mod_on_init(loaded_index)
|
||||
end
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
_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
|
||||
sub_commands['help'](e.player_index)
|
||||
return
|
||||
end
|
||||
local sub_command, rest = e.parameter:match('(.-)%s(.*)')
|
||||
if not sub_command then
|
||||
sub_command = e.parameter
|
||||
end
|
||||
|
||||
local f = sub_commands[sub_command]
|
||||
if f then f(e.player_index, rest) end
|
||||
if caller.admin then
|
||||
f = admin_commands[sub_command]
|
||||
if f then f(e.player_index, rest) end
|
||||
end
|
||||
end)
|
||||
|
||||
--]===])
|
||||
|
||||
return true
|
||||
@@ -0,0 +1,377 @@
|
||||
local hotpatch_tools = require 'hotpatch.mod-tools'
|
||||
hotpatch_tools.static_mod('hotpatch-gui', '1.0.4', [===[
|
||||
--[[
|
||||
|
||||
Copyright 2018 Chrisgbk
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
]]
|
||||
-- MIT License, https://opensource.org/licenses/MIT
|
||||
|
||||
local hotpatch_tools = require 'hotpatch.mod-tools'
|
||||
local mod_gui = require 'mod-gui'
|
||||
|
||||
--load private API
|
||||
-- mod installation/uninstallation support functions
|
||||
-- These take a mod NAME as a first argument
|
||||
local install_mod = hotpatch_tools.install_mod
|
||||
local find_installed_mod = hotpatch_tools.find_installed_mod
|
||||
local install_mod_file = hotpatch_tools.install_mod_file
|
||||
local uninstall_mod = hotpatch_tools.uninstall_mod
|
||||
|
||||
-- mod interaction functions
|
||||
-- These take a LOADED INDEX as a first argument, except load_mod, which takes an INSTALLED INDEX
|
||||
local load_mod = hotpatch_tools.load_mod
|
||||
local find_loaded_mod = hotpatch_tools.find_loaded_mod
|
||||
local run_mod = hotpatch_tools.run_mod
|
||||
local reset_mod = hotpatch_tools.reset_mod
|
||||
local reset_mod_events = hotpatch_tools.reset_mod_events
|
||||
local register_mod_events = hotpatch_tools.register_mod_events
|
||||
local unload_mod = hotpatch_tools.unload_mod
|
||||
|
||||
-- internal callbacks when a mod registers events
|
||||
local register_event = hotpatch_tools.register_event
|
||||
local register_nth_tick = hotpatch_tools.register_nth_tick
|
||||
local register_on_tick = hotpatch_tools.register_on_tick
|
||||
|
||||
-- mod bootstrap functions
|
||||
-- These take a LOADED INDEX as a first argument
|
||||
local mod_on_init = hotpatch_tools.mod_on_init
|
||||
local mod_on_load = hotpatch_tools.mod_on_load
|
||||
local mod_on_configuration_changed = hotpatch_tools.mod_on_configuration_changed
|
||||
|
||||
local static_mods = hotpatch_tools.static_mods
|
||||
local console = hotpatch_tools.console
|
||||
local debug_log = hotpatch_tools.debug_log
|
||||
local loaded_mods = hotpatch_tools.loaded_mods
|
||||
local installed_mods = hotpatch_tools.installed_mods
|
||||
|
||||
script.on_event(defines.events.on_player_joined_game, 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 button = top['hotpatch-button']
|
||||
if button then
|
||||
button.destroy()
|
||||
end
|
||||
local menu = left['hotpatch-menu']
|
||||
if menu then
|
||||
menu.destroy()
|
||||
end
|
||||
|
||||
local IDE = center['hotpatch-IDE']
|
||||
if IDE then
|
||||
IDE.destroy()
|
||||
end
|
||||
|
||||
local main = center['hotpatch-main']
|
||||
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)
|
||||
|
||||
local on_gui_click_handlers
|
||||
local on_gui_selection_state_changed_handlers
|
||||
|
||||
on_gui_click_handlers = {
|
||||
['hotpatch-button'] = function(e)
|
||||
--on_gui_click_handlers['hotpatch-menu.IDE'](e)
|
||||
--do return end
|
||||
local player = game.players[e.player_index]
|
||||
local top = mod_gui.get_button_flow(player)
|
||||
local left = mod_gui.get_frame_flow(player)
|
||||
local center = player.gui.center
|
||||
|
||||
local menu = left['hotpatch-menu']
|
||||
if not menu then
|
||||
menu = left.add{type = 'frame', name = 'hotpatch-menu', direction = 'vertical'}
|
||||
menu.add{type = 'button', name = 'hotpatch-menu.IDE', caption = 'Mod IDE', tooltip = 'Open debugging GUI'}
|
||||
menu.add{type = 'button', name = 'hotpatch-menu.console', caption = 'Mod Console', tooltip = 'Open debugging console'}
|
||||
menu.visible = true
|
||||
return
|
||||
end
|
||||
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.visible = not menu.visible
|
||||
|
||||
on_gui_click_handlers[e.element.name](e)
|
||||
end,
|
||||
['hotpatch-menu.IDE'] = function(e)
|
||||
local player = game.players[e.player_index]
|
||||
local top = mod_gui.get_button_flow(player)
|
||||
local left = mod_gui.get_frame_flow(player)
|
||||
local center = player.gui.center
|
||||
|
||||
local IDE = center['hotpatch-IDE']
|
||||
if not IDE then
|
||||
IDE = center.add{type = 'frame', name = 'hotpatch-IDE', direction = 'vertical', caption = 'Hotpatch IDE', style = mod_gui.frame_style}
|
||||
local top_flow = IDE.add{type = 'flow', name = 'hotpatch-IDE-top', direction = 'horizontal'}
|
||||
top_flow.add{type = 'label', name = 'hotpatch-IDE-mod-label', caption = 'Mod: '}
|
||||
local IDE_dropdown = top_flow.add{type = 'drop-down', name = 'hotpatch-IDE-mod-selector'}
|
||||
top_flow.add{type = 'label', name = 'hotpatch-IDE-mod-version', caption = 'Version: No mod selected'}
|
||||
local IDE_table = IDE.add{type = 'table', name = 'hotpatch-IDE-table', column_count = 2}
|
||||
local files = IDE_table.add{type = 'scroll-pane', name = 'hotpatch-IDE-files', direction='vertical'}
|
||||
files.style.height = 600
|
||||
files.vertical_scroll_policy = 'always'
|
||||
files = files.add{type = 'table', name = 'hotpatch-IDE-files-table', column_count = 1}
|
||||
local code = IDE_table.add{type = 'text-box', name = 'hotpatch-IDE-code'}
|
||||
code.word_wrap = true
|
||||
files.style.width = 400
|
||||
files.style.cell_padding = 0
|
||||
files.style.top_padding = 0
|
||||
files.style.bottom_padding = 0
|
||||
code.style.width = 600
|
||||
code.style.height = 600
|
||||
IDE.visible = false
|
||||
end
|
||||
|
||||
local IDE_dropdown = IDE['hotpatch-IDE-top']['hotpatch-IDE-mod-selector']
|
||||
IDE_dropdown.clear_items()
|
||||
for _, v in ipairs(installed_mods) do
|
||||
IDE_dropdown.add_item(v.name)
|
||||
end
|
||||
|
||||
IDE.visible = not IDE.visible
|
||||
if IDE.visible then
|
||||
player.opened = IDE
|
||||
else
|
||||
player.opened = nil
|
||||
end
|
||||
end,
|
||||
['hotpatch-menu.console'] = function(e)
|
||||
local player = game.players[e.player_index]
|
||||
local top = mod_gui.get_button_flow(player)
|
||||
local left = mod_gui.get_frame_flow(player)
|
||||
local center = player.gui.center
|
||||
|
||||
local hotpatch_console = center['hotpatch-console']
|
||||
if not hotpatch_console then
|
||||
hotpatch_console = center.add{type = 'frame', name = 'hotpatch-console', direction = 'vertical', caption = 'Hotpatch Console'}
|
||||
local top_flow = hotpatch_console.add{type = 'flow', name = 'hotpatch-console-top', direction = 'horizontal'}
|
||||
top_flow.add{type = 'label', name = 'hotpatch-console-mod-label', caption = 'Mod: '}
|
||||
local console_dropdown = top_flow.add{type = 'drop-down', name = 'hotpatch-console-mod-selector'}
|
||||
top_flow.add{type = 'label', name = 'hotpatch-console-mod-version', caption = 'Version: No mod selected'}
|
||||
local bottom_flow = hotpatch_console.add{type = 'flow', name = 'hotpatch-console-bottom', direction = 'vertical'}
|
||||
local 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'}
|
||||
bottom_input_flow.add{type = 'button', name = 'hotpatch-console-run', caption = 'Run', tooltip = 'Run Lua Code'}
|
||||
output.style.width = 800
|
||||
output.style.height = 600
|
||||
input.style.width = 600
|
||||
hotpatch_console.visible = false
|
||||
end
|
||||
|
||||
local console_dropdown = hotpatch_console['hotpatch-console-top']['hotpatch-console-mod-selector']
|
||||
console_dropdown.clear_items()
|
||||
for _, v in ipairs(loaded_mods) do
|
||||
console_dropdown.add_item(v.name)
|
||||
end
|
||||
|
||||
hotpatch_console.visible = not hotpatch_console.visible
|
||||
if hotpatch_console.visible then
|
||||
player.opened = hotpatch_console
|
||||
else
|
||||
player.opened = nil
|
||||
end
|
||||
end,
|
||||
['hotpatch-console-run'] = function(e)
|
||||
local player = game.players[e.player_index]
|
||||
|
||||
local top = mod_gui.get_button_flow(player)
|
||||
local left = mod_gui.get_frame_flow(player)
|
||||
local center = player.gui.center
|
||||
|
||||
local hotpatch_console = center['hotpatch-console']
|
||||
local output = hotpatch_console['hotpatch-console-bottom']['hotpatch-console-output']
|
||||
local input = hotpatch_console['hotpatch-console-bottom']['hotpatch-console-bottom-input']['hotpatch-console-input']
|
||||
local console_dropdown = hotpatch_console['hotpatch-console-top']['hotpatch-console-mod-selector']
|
||||
|
||||
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(...)
|
||||
for k, v in pairs(t) do
|
||||
t[k] = tostring(v)
|
||||
end
|
||||
local text = table.concat(t, '\t')
|
||||
output.text = table.concat{output.text, '\n', text}
|
||||
end
|
||||
|
||||
env.log = function(text)
|
||||
output.text = table.concat{output.text, '\n', text}
|
||||
end
|
||||
|
||||
local success
|
||||
local code, err = load(input.text, input.text, 't', env)
|
||||
if code then
|
||||
success, err = pcall(code)
|
||||
if not success then
|
||||
output.text = table.concat{output.text, '\n', err}
|
||||
end
|
||||
else
|
||||
output.text = table.concat{output.text, '\n', err}
|
||||
end
|
||||
|
||||
env.print = old_print
|
||||
env.log = old_log
|
||||
end,
|
||||
['hotpatch-IDE-file'] = function(e)
|
||||
local player = game.players[e.player_index]
|
||||
local top = mod_gui.get_button_flow(player)
|
||||
local left = mod_gui.get_frame_flow(player)
|
||||
local center = player.gui.center
|
||||
|
||||
local element = e.element
|
||||
local previous
|
||||
for _, v in pairs(center['hotpatch-IDE']['hotpatch-IDE-table']['hotpatch-IDE-files']['hotpatch-IDE-files-table'].children) do
|
||||
if table.compare(v.style.font_color or {}, {r=1.0, g=1.0, b=0.0, a=1.0}) then
|
||||
previous = v
|
||||
break
|
||||
end
|
||||
end
|
||||
if previous then
|
||||
previous.style.font_color = {r=1.0, g=1.0, b=1.0, a=1.0}
|
||||
end
|
||||
element.style.font_color = {r=1.0, g=1.0, b=0.0, a=1.0}
|
||||
local file = element.name:match('.-%.(.*)')
|
||||
local selected = center['hotpatch-IDE']['hotpatch-IDE-top']['hotpatch-IDE-mod-selector']
|
||||
local mod_name = selected.items[selected.selected_index]
|
||||
local code = center['hotpatch-IDE']['hotpatch-IDE-table']['hotpatch-IDE-code']
|
||||
local index = find_installed_mod(mod_name)
|
||||
if index then
|
||||
local mod = installed_mods[index]
|
||||
if file == 'control' then
|
||||
code.text = mod.code
|
||||
else
|
||||
code.text = mod.files[file]
|
||||
end
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
script.on_event(defines.events.on_gui_click, function(e)
|
||||
local element = e.element
|
||||
if element.valid then
|
||||
local name = element.name:match('([^%.]*)%.?.-')
|
||||
local handler = on_gui_click_handlers[name]
|
||||
if handler then handler(e) end
|
||||
end
|
||||
end)
|
||||
|
||||
on_gui_selection_state_changed_handlers = {
|
||||
['hotpatch-console-mod-selector'] = function(e)
|
||||
local player = game.players[e.player_index]
|
||||
local top = mod_gui.get_button_flow(player)
|
||||
local left = mod_gui.get_frame_flow(player)
|
||||
local center = player.gui.center
|
||||
|
||||
local element = e.element
|
||||
local name = element.items[element.selected_index]
|
||||
if name then
|
||||
local index = find_loaded_mod(name)
|
||||
if index then
|
||||
local mod = loaded_mods[index]
|
||||
local version_label = center['hotpatch-console']['hotpatch-console-top']['hotpatch-console-mod-version']
|
||||
version_label.caption = 'Version: ' .. mod.version
|
||||
local hotpatch_console = center['hotpatch-console']
|
||||
local output = hotpatch_console['hotpatch-console-bottom']['hotpatch-console-output']
|
||||
output.text = ''
|
||||
end
|
||||
end
|
||||
end,
|
||||
['hotpatch-IDE-mod-selector'] = function(e)
|
||||
local player = game.players[e.player_index]
|
||||
local top = mod_gui.get_button_flow(player)
|
||||
local left = mod_gui.get_frame_flow(player)
|
||||
local center = player.gui.center
|
||||
|
||||
local element = e.element
|
||||
|
||||
local name = element.items[element.selected_index]
|
||||
if name then
|
||||
local index = find_installed_mod(name)
|
||||
if index then
|
||||
local mod = installed_mods[index]
|
||||
local version_label = center['hotpatch-IDE']['hotpatch-IDE-top']['hotpatch-IDE-mod-version']
|
||||
version_label.caption = 'Version: ' .. mod.version
|
||||
local list = center['hotpatch-IDE']['hotpatch-IDE-table']['hotpatch-IDE-files']['hotpatch-IDE-files-table']
|
||||
list.clear()
|
||||
local file = list.add{type = 'label', style = 'hoverable_bold_label', caption = 'control', name = 'hotpatch-IDE-file.control'}
|
||||
file.style.bottom_padding = 0
|
||||
file.style.top_padding = 0
|
||||
for k, _ in pairs(mod.files) do
|
||||
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
|
||||
end
|
||||
local code = center['hotpatch-IDE']['hotpatch-IDE-table']['hotpatch-IDE-code']
|
||||
code.text = ''
|
||||
end
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
script.on_event(defines.events.on_gui_selection_state_changed, function(e)
|
||||
local element = e.element
|
||||
local handler = on_gui_selection_state_changed_handlers[element.name]
|
||||
if handler then handler(e) end
|
||||
end)
|
||||
|
||||
script.on_event(defines.events.on_gui_closed, function(e)
|
||||
local player = game.players[e.player_index]
|
||||
local element = e.element
|
||||
if e.gui_type == defines.gui_type.custom then
|
||||
if element and element.valid then
|
||||
if element.name == 'hotpatch-IDE' then
|
||||
if element.visible then
|
||||
player.play_sound{path = "utility/gui_click"}
|
||||
end
|
||||
element.visible = false
|
||||
elseif element.name == 'hotpatch-console' then
|
||||
if element.visible then
|
||||
player.play_sound{path = "utility/gui_click"}
|
||||
end
|
||||
element.visible = false
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
--]===])
|
||||
|
||||
return true
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,322 @@
|
||||
local hotpatch_tools = require 'hotpatch.mod-tools'
|
||||
hotpatch_tools.static_mod('hotpatch-remote-interface', '1.0.2', [===[
|
||||
--[[
|
||||
|
||||
Copyright 2018 Chrisgbk
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
]]
|
||||
-- MIT License, https://opensource.org/licenses/MIT
|
||||
|
||||
local hotpatch_tools = require 'hotpatch.mod-tools'
|
||||
|
||||
--load private API
|
||||
-- mod installation/uninstallation support functions
|
||||
-- These take a mod NAME as a first argument
|
||||
local install_mod = hotpatch_tools.install_mod
|
||||
local find_installed_mod = hotpatch_tools.find_installed_mod
|
||||
local install_mod_file = hotpatch_tools.install_mod_file
|
||||
local uninstall_mod = hotpatch_tools.uninstall_mod
|
||||
|
||||
-- mod interaction functions
|
||||
-- These take a LOADED INDEX as a first argument, except load_mod, which takes an INSTALLED INDEX
|
||||
local load_mod = hotpatch_tools.load_mod
|
||||
local find_loaded_mod = hotpatch_tools.find_loaded_mod
|
||||
local run_mod = hotpatch_tools.run_mod
|
||||
local reset_mod = hotpatch_tools.reset_mod
|
||||
local reset_mod_events = hotpatch_tools.reset_mod_events
|
||||
local register_mod_events = hotpatch_tools.register_mod_events
|
||||
local unload_mod = hotpatch_tools.unload_mod
|
||||
|
||||
-- internal callbacks when a mod registers events
|
||||
local register_event = hotpatch_tools.register_event
|
||||
local register_nth_tick = hotpatch_tools.register_nth_tick
|
||||
local register_on_tick = hotpatch_tools.register_on_tick
|
||||
|
||||
-- mod bootstrap functions
|
||||
-- These take a LOADED INDEX as a first argument
|
||||
local mod_on_init = hotpatch_tools.mod_on_init
|
||||
local mod_on_load = hotpatch_tools.mod_on_load
|
||||
local mod_on_configuration_changed = hotpatch_tools.mod_on_configuration_changed
|
||||
|
||||
local static_mods = hotpatch_tools.static_mods
|
||||
local console = hotpatch_tools.console
|
||||
local debug_log = hotpatch_tools.debug_log
|
||||
local loaded_mods = hotpatch_tools.loaded_mods
|
||||
local installed_mods = hotpatch_tools.installed_mods
|
||||
|
||||
local remote_interface = {}
|
||||
|
||||
--IMPORTANT: WHEN COPY PASTING CODE TO CONSOLE THE CODE MUST HAVE SINGLE LINE COMMENTS REMOVED
|
||||
-- FACTORIO CONSOLE STRIPS LINEFEEDS WHICH MAKES ALL THE CODE BECOME COMMENTED OUT
|
||||
--TODO: gate these behind admin permissions; if (game.player and game.player.admin) should work for console commands?
|
||||
|
||||
debug_log({'hotpatch-info.remote-installing'})
|
||||
|
||||
remote_interface['install'] = function(mod_name, mod_version, mod_code, mod_files, only_install)
|
||||
-- this installs a new mod and runs on_init, then registers events
|
||||
-- Note that mods may expect that certain events haven't been called yet when their on_init is ran
|
||||
-- This may prevent them from functioning properly, without manually calling the events they expect
|
||||
-- examples: on_player_created
|
||||
local caller = game.player or console
|
||||
if caller.admin then
|
||||
if mod_files then
|
||||
for k,_ in pairs(mod_files) do
|
||||
if k == 'control' then
|
||||
caller.print('ERROR: control.lua loaded twice for mod ' .. mod_name)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
local installed_index = find_installed_mod(mod_name)
|
||||
local loaded_index = find_loaded_mod(mod_name)
|
||||
if installed_index then
|
||||
local old_version = installed_mods[installed_index].version
|
||||
if old_version then
|
||||
debug_log({'hotpatch-warning.already-exists', mod_name, old_version})
|
||||
debug_log({'hotpatch-warning.reinstalling', mod_name, old_version})
|
||||
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)
|
||||
if installed_index then
|
||||
if not load_mod(installed_index) then
|
||||
caller.print('compilation failed for mod ' .. mod_name)
|
||||
return
|
||||
end
|
||||
loaded_index = find_loaded_mod(mod_name)
|
||||
if loaded_index then
|
||||
if not run_mod(loaded_index) then
|
||||
caller.print('execution failed for mod ' .. mod_name)
|
||||
unload_mod(loaded_index)
|
||||
return
|
||||
end
|
||||
if not mod_on_init(loaded_index) then
|
||||
caller.print('on_init failed for mod ' .. mod_name)
|
||||
unload_mod(loaded_index)
|
||||
return
|
||||
end
|
||||
-- TODO: notify all mods
|
||||
-- TODO: determine vanilla behaviour and replicate it
|
||||
if not mod_on_configuration_changed(loaded_index, {mod_changes = {mod_name={new_version=mod_version}}}) then
|
||||
caller.print('on_configuration_changed failed for mod ' .. mod_name)
|
||||
unload_mod(loaded_index)
|
||||
return
|
||||
end
|
||||
else
|
||||
end
|
||||
else
|
||||
end
|
||||
end
|
||||
else
|
||||
caller.print('You must be an admin to run this command.')
|
||||
end
|
||||
end
|
||||
|
||||
remote_interface['run'] = function(mod_name)
|
||||
-- this runs on_init, then registers events
|
||||
-- Note that mods may expect that certain events haven't been called yet when their on_init is ran
|
||||
-- This may prevent them from functioning properly, without manually calling the events they expect
|
||||
-- examples: on_player_created
|
||||
local caller = game.player or console
|
||||
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)
|
||||
return
|
||||
end
|
||||
loaded_index = find_loaded_mod(mod_name)
|
||||
else
|
||||
if loaded_index then
|
||||
mod = loaded_mods[loaded_index]
|
||||
version = mod.version
|
||||
debug_log({'hotpatch-warning.already-loaded', mod_name, version})
|
||||
debug_log({'hotpatch-warning.reinitializing', mod_name, version})
|
||||
end
|
||||
end
|
||||
|
||||
if loaded_index then
|
||||
if not run_mod(loaded_index) then
|
||||
caller.print('execution failed for mod ' .. mod_name)
|
||||
unload_mod(loaded_index)
|
||||
return
|
||||
end
|
||||
if not mod_on_init(loaded_index) then
|
||||
caller.print('on_init failed for mod ' .. mod_name)
|
||||
unload_mod(loaded_index)
|
||||
return
|
||||
end
|
||||
-- TODO: notify all mods
|
||||
-- TODO: determine vanilla behaviour and replicate it
|
||||
if not mod_on_configuration_changed(loaded_index, {mod_changes = {mod_name={new_version=version}}}) then
|
||||
caller.print('on_configuration_changed failed for mod ' .. mod_name)
|
||||
unload_mod(loaded_index)
|
||||
return
|
||||
end
|
||||
end
|
||||
else
|
||||
caller.print('You must be an admin to run this command.')
|
||||
end
|
||||
end
|
||||
|
||||
remote_interface['update'] = function(mod_name, mod_version, mod_code, mod_files)
|
||||
-- this updates an existing mod
|
||||
-- the current mods events are de-registered, the new code is installed, on_load is triggered, and then events are registered
|
||||
-- finally, the mod is informed of the update, so it can run migrations from the previous version
|
||||
-- TODO: validation
|
||||
local caller = game.player or console
|
||||
if caller.admin then
|
||||
if mod_files then
|
||||
for k,_ in pairs(mod_files) do
|
||||
if k == 'control' then
|
||||
caller.print('ERROR: control.lua loaded twice for mod ' .. mod_name)
|
||||
return
|
||||
end
|
||||
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
|
||||
old_version = loaded_mods[loaded_index].version
|
||||
unload_mod(loaded_index)
|
||||
end
|
||||
install_mod(mod_name, mod_version, mod_code, mod_files)
|
||||
installed_index = find_installed_mod(mod_name)
|
||||
if not load_mod(installed_index) then
|
||||
caller.print('compilation failed for mod ' .. mod_name)
|
||||
return
|
||||
end
|
||||
loaded_index = find_loaded_mod(mod_name)
|
||||
if not run_mod(loaded_index) then
|
||||
caller.print('execution failed for mod ' .. mod_name)
|
||||
unload_mod(loaded_index)
|
||||
return
|
||||
end
|
||||
if old_version then
|
||||
if not mod_on_load(loaded_index) then
|
||||
caller.print('on_load failed for mod ' .. mod_name)
|
||||
unload_mod(loaded_index)
|
||||
return
|
||||
end
|
||||
-- The mod must do any migrations here
|
||||
-- TODO: notify all mods
|
||||
if not mod_on_configuration_changed(loaded_index, {mod_changes = {mod_name={old_version=old_version, new_version=mod_version}}}) then
|
||||
caller.print('on_configuration_changed failed for mod ' .. mod_name)
|
||||
unload_mod(loaded_index)
|
||||
return
|
||||
end
|
||||
else
|
||||
-- first time install
|
||||
if not mod_on_init(loaded_index) then
|
||||
caller.print('on_init failed for mod ' .. mod_name)
|
||||
unload_mod(loaded_index)
|
||||
return
|
||||
end
|
||||
-- TODO: notify all mods
|
||||
if not mod_on_configuration_changed(loaded_index, {mod_changes = {mod_name={new_version=mod_version}}}) then
|
||||
caller.print('on_configuration_changed failed for mod ' .. mod_name)
|
||||
unload_mod(loaded_index)
|
||||
return
|
||||
end
|
||||
end
|
||||
else
|
||||
caller.print('You must be an admin to run this command.')
|
||||
end
|
||||
end
|
||||
|
||||
remote_interface['install_mod_file'] = function(mod_name, mod_file, mod_file_code)
|
||||
local caller = game.player or console
|
||||
if caller.admin then
|
||||
install_mod_file(mod_name, mod_file, mod_file_code)
|
||||
else
|
||||
caller.print('You must be an admin to run this command.')
|
||||
end
|
||||
end
|
||||
|
||||
remote_interface['exec'] = function(mod_name, exec_code)
|
||||
local caller = game.player or console
|
||||
if caller.admin then
|
||||
local loaded_index = find_loaded_mod(mod_name)
|
||||
local env = loaded_mods[loaded_index].env
|
||||
|
||||
local success
|
||||
local code, err = load(exec_code, exec_code, 't', env)
|
||||
if code then
|
||||
success, err = pcall(code)
|
||||
if not success then
|
||||
debug_log(err)
|
||||
end
|
||||
else
|
||||
debug_log(err)
|
||||
end
|
||||
else
|
||||
caller.print('You must be an admin to run this command.')
|
||||
end
|
||||
end
|
||||
|
||||
--TODO: most of this function
|
||||
remote_interface['clean'] = function(mod_name)
|
||||
-- Removes ALL mod data and reinitializes
|
||||
-- doesn't remove things a mod may add like surfaces, etc
|
||||
local caller = game.player or console
|
||||
if caller.admin then
|
||||
--mod_reset(mod_name)
|
||||
--run_mod(mod_name)
|
||||
--mod_on_init(mod_name)
|
||||
local loaded_index = find_loaded_mod(mod_name)
|
||||
local mod = loaded_mods[loaded_index]
|
||||
local version = mod.version
|
||||
mod_on_configuration_changed(loaded_index, {mod_changes = {mod_name={new_version=version}}})
|
||||
else
|
||||
caller.print('You must be an admin to run this command.')
|
||||
end
|
||||
end
|
||||
|
||||
remote_interface['uninstall'] = function(mod_name)
|
||||
-- Uninstalls a mod
|
||||
local caller = game.player or console
|
||||
if caller.admin then
|
||||
uninstall_mod(mod_name)
|
||||
else
|
||||
caller.print('You must be an admin to run this command.')
|
||||
end
|
||||
end
|
||||
|
||||
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')
|
||||
end
|
||||
]===]
|
||||
|
||||
local updated_code = ....
|
||||
remote.call('hotpatch', 'update', 'test', '1.0.1', updated_code)
|
||||
|
||||
|
||||
]]
|
||||
|
||||
return true
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Hierdie is Factorio vryspel. Jou taak is om 'n vuurpyl in die buitenste ruimte in te stuur. Doen dit deur die bou van 'n Vuurpyl Silo en van daar af 'n vuurpyl te lanseer met a satelliet aan boord. Jy sal baie gevorderde tegnologiëe moet navors om die Vuurpyl Silo te ontsluit. Begin klein, werk jou pad op met outomatisering en moenie vergeet om jouself te beskerm teen die inboorlinge nie.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=.نييلصألا ناكسلا نم كسفن ةيامح ىسنت الو ةياهنلا ىلإ لصتل كتقيرطب لمعأو ،رفصلا نم أدبتس .خيراوصلا عماوص حتف لجأ نم ةمدقتم ايجولونكت نع ثحبلل جاتحت فوس .ةيعانصلا رامقألاب خيراوصلا قالطإو خيراوصلا عماوص ءانب قيرط نع كلذب مايقلا عيطتست .ءاضفلل اخوراص قلطت نا يه كتمهم .Factorio ـل رحلا بعللا وه اذه
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Гэта рэжым свабоднай гульні Factorio. Вашая мэта - запусціць ракету ў космас. Дзеля гэтага вам трэба пабудаваць ракетную шахту і саму ракету з спадарожнікам. Вы мусіце даследваць перадавыя тэхналогіі, каб навучыцца будаваць ракетную шахту. Пачынайце з малога, працуйце над аўтаматызацыяй вытворчасці, даследуйце новыя тэхналогіі і не забывайцеся на абарону ад "тутэйшых"!
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Това е режимът на свободна игра. Задачата ви е да изстреляте ракета в космоса. За това ще е нужно да построите ракетна шахта и изстреляте ракетата със спътник. Ще трябва да проучите развити технологии, които ще отключат ракетната шахта. Започнете с малко и постепенно автоматизирайте процеса, като не забравяте да се защитавате от местните форми на живот.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Aquest és el mode lliure del Factorio. La vostra tasca és construir un coet i enviar-lo a l'espai. Ho haureu de fer construint un centre de coets espacials i llençant un coet amb un satèl·lit. Necessitareu investigar tecnologies avançades per poder-ho fer. Aneu pas a pas, avanceu cap a l'automatització i no oblideu protegir-vos dels natius.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Toto je volná hra Factorio. Tvým úkolem je vypustit raketu do vesmíru. Postav raketové silo a vypusť raketu se satelitem. Pro odemknutí raketového sila budeš muset vyzkoumat pokročilé technologie. Začni v malém, postupuj s automatizací a nezapomeň se chránit před domorodci.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Det her er Factorio fri leg. Din opgave er at affyre en raket for at udforske rummet. For at gøre det skal du bygge en raket silo og affyre en raket med en satellit i. Du bliver nød til at udforske avancerede teknologier for at at kunne bygge en raket silo. Start småt, og arbejd din vej op med hjælp af automatisering, glem ikke at beskytte dig fra de indfødte.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Dies ist das freie Spiel von Factorio. Deine Aufgabe ist es, eine Rakete in den Weltraum zu starten. Dazu musst Du ein Raketensilo bauen und eine Rakete mit einem Satelliten starten. Du wirst erweiterte Technologien erforschen müssen, um das Raketensilo freizuschalten. Fange klein an, arbeite Dich durch Automatisierung hoch und vergiss nicht, Dich vor den Einheimischen zu schützen.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Αυτό είναι το ελεύθερο παιχνίδι του Factorio. Ο στόχος σου είναι να εκτοξεύσεις έναν πύραυλο στο διάστημα. Αυτό το κάνεις κατασκευάζοντας ένα σιλό πυραύλου και εκτοξεύοντας έναν πυραύλο με δορυφόρο. Θα πρέπει να ερευνήσεις προηγμένες τεχνολογίες για να ξεκλειδώσεις το σιλό πυραύλου. Ξεκινάς περιορισμένος, δούλεψε με αυτοματισμούς για την άνοδο σου και μην ξεχνάς να προστατέψεις τον εαυτό σου από τους ντόπιους.
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
msg-intro=This is the Factorio freeplay. Your task is to launch a rocket into space. Do this by constructing a Rocket Silo and launching a rocket with a satellite. You will need to research advanced technologies in order to unlock the Rocket Silo. Start small, work your way up with automation and don't forget to protect yourself from the natives.
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
[hotpatch]
|
||||
log=@__1__:__2__] __3__
|
||||
log-mod=@__1__:__2__][__4__] __3__
|
||||
severe=SEVERE ERROR: __1__
|
||||
error=ERROR: __1__
|
||||
warning=WARNING: __1__
|
||||
info=INFO: __1__
|
||||
verbose=INFO: __1__
|
||||
trace=TRACE: __1__
|
||||
|
||||
[hotpatch-info]
|
||||
logging-enabled=Logging enabled
|
||||
metatable-installed=_ENV metatable installed
|
||||
complete=__1__ Complete!
|
||||
on-init=initializing...
|
||||
on-load=loading...
|
||||
on-configuration-changed=mod configuration changed...
|
||||
installing-included-mods=installing included mods...
|
||||
loading-installed-mods=loading installed mods...
|
||||
loading-libs=loading Factorio.data.core.lualib...
|
||||
loading-library=loading library: __1__
|
||||
uninstalling=Uninstalling mod...
|
||||
installing=Installing version __1__...
|
||||
installing-file=Installing file __1__...
|
||||
script-shim=setting up mod script shim...
|
||||
setting-env=setting up mod _ENV...
|
||||
loading=loading...
|
||||
unloading=unloading...
|
||||
running=running...
|
||||
must-be-admin=You must be an admin to run this command
|
||||
remote-installing=installing remote interface
|
||||
|
||||
[hotpatch-trace]
|
||||
nil-var-access=_ENV nil variable access: __1__
|
||||
nil-var-assignment=_ENV variable assignment: __1__
|
||||
event-registering=registering events...
|
||||
on-tick-event-registered=registered on_tick event...
|
||||
on-event-registered=registered event __1__...
|
||||
on-nth-tick-event-registered=registered nth_tick event __1__...
|
||||
event-running=running event __1__...
|
||||
nth-tick-event-running=running nth_event __1__...
|
||||
mod-on-init=running on_init...
|
||||
mod-on-load=running on_load...
|
||||
mod-on-configuration-changed=running on_configuration_changed...
|
||||
adding-event=adding event __1__
|
||||
adding-nth-tick-event=adding nth_tick event __1__
|
||||
caching-event=caching event: __1__...
|
||||
caching-nth-tick-event=caching nth_tick event: __1__...
|
||||
event-processing=processing event __1__...
|
||||
nth-tick-event-processing=processing nth_tick event __1__...
|
||||
nth-tick-handler-added=added nth_tick handler: __1__
|
||||
nth-tick-handler-removed=removed nth_tick handler: __1__
|
||||
on-tick-handler-added=added on_tick handler
|
||||
on-tick-handler-removed=removed on_tick handler
|
||||
on-event-handler-added=added event handler: __1__
|
||||
on-event-handler-removed=removed event handler: __1__
|
||||
cached-load-require=loading cached require'd file: __1__...
|
||||
load-require=loading require'd file: __1__...
|
||||
load-core-lib=loading from Factorio.data.core.lualib: __1__...
|
||||
|
||||
[hotpatch-warning]
|
||||
contains-comments=mod code contains comments!
|
||||
contains-comments-no-lf=mod contains comments and no linefeed!
|
||||
contains-comments-console=comments from console will comment out the entire code!
|
||||
reset-events-not-running=tried to reset events for mod that isn't running!
|
||||
remote-interface-exists=remote interface __1__ already exists, removing...
|
||||
command-exists=command __1__ already exists, removing...
|
||||
already-exists=mod already exists: __1__ __2__
|
||||
reinstalling=reinstalling mod in-place: __1__ __2__
|
||||
already-loaded=mod already loaded: __1__ __2__
|
||||
reinitializing=reinitializing: __1__ __2__
|
||||
|
||||
[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
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Tio estas la Factorio Libera Ludado. Via tasko estas ĵeti raketon en spacon. Fari tiun per konstruanta Raketo Silo kaj ĵeti raketon kun satelito. Vi bezonos esplori altnivelaj teknologioj por malŝlosi la Raketo Silo. Start malgranda, labori vian vojon supren per aŭtomatigo kaj ne forgesu protekti vin mem de la indiĝenoj.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Este es el modo de juego libre de Factorio. Tu tarea es la de lanzar un cohete al espacio. Logra esto construyendo un Silo de Cohetes y lanzando un cohete con un satélite. Necesitarás investigar tecnologías avanzadas para poder desbloquear el Silo de Cohetes. Comienza poco a poco, trabaja en tu sistema de automatización y no te olvides de protegerte de los nativos.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=See on Factorio vabamäng. Sinu eesmärgiks on saata rakett avakosmosesse. Seda saad teha ehitades Raketi Silotorni ja lennutades Raketti koos satelliidiga. Selleks pead õppima selgeks keerukad tehnoloogiaharud, et Raketi Silotorni lahti lukustada. Alusta väikselt ning liigu edasi automatiseerimise abil ja ära unusta ennast kaitsta kohalike eluvormide eest.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Tämä on Factorion vapaapelimuoto. Tavoitteesi on lähettää satelliitti ulkoavaruuteen rakentamalla rakettisiilo ja laukaisemalla raketti. Rakettisiilon voit rakentaa vasta, kun olet kehittänyt siihen tarvittavat huipputeknologiat. Aloita pienestä ja automatisoi kaikki tarvittava, äläkä unohda suojella itseäsi alkuasukkailta.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Il s'agit du mode de jeu libre de Factorio. Votre tâche consiste à lancer une fusée dans l'espace. Ceci en construisant un silo à fusée, et en lançant un satellite avec la fusée. Vous aurez besoin de rechercher des technologies de pointe afin de débloquer le silo. Commencez petit, évoluez en automatisant et n'oubliez pas de vous protéger contre les autochtones.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=.םיימוקמה םירוציה ינפמ םכמצע לע ןגהל וחכשת לאו ,יטמוטוא ןוכימ תועצמאב םכלש הישעתה תא וביחרה ,ןטקב וליחתה .ןקתמה תיינב תא רשפאל ידכ תומדקתמ תויגולונכט רוקחל םכילע היהי .ןייוול ליכמה ליט ותועצמאב רגשלו םיליט רוגיש ןקתמ תונבל םכילע .ללחל ליט רגשל איה םכתמישמ .ויִרֹוטְקַפ לש ישפוחה שיחרתה והז
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Ez a Factorio szabad játék módja. A feladatod az, hogy kilőj egy rakétát az űr felderítésére. Ehhez egy Rakétasilót kell építened, melyből aztán egy műholddal felszerelt rakétát kell kilőnöd. A Rakétasiló kifejlesztéséhez különféle technológiákat kell fejlesztened. Kezdj kicsiben és küzdd fel magad az automatizálásban. Ne felejts el védekezni a honos fajok ellen.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Questa è la modalità libera di Factorio. Il tuo obiettivo è quello di lanciare il razzo nello spazio. Completalo costruendo un Silo per Razzi e lanciando il razzo con il satellite. Avrai bisogno di ricercare tecnologie avanzate per riuscire a sbloccare il Silo per Razzi. Inizia in piccolo, espandi con l'automazione e non dimenticare di proteggerti dai nativi.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=これは、Factorioのフリープレイです。ロケットサイロを建造し、衛星を載せたロケットを宇宙へ打ち上げるのがあなたの任務です。ロケットサイロをアンロックするためには高度な技術研究をする必要があります。小規模に始め、自動化しつつ進み、原住生物から身を守ることも忘れないでください。
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Factorio 자유플레이입니다. 당신의 목표는 우주로 로켓을 발사하는 것입니다. 로켓 격납고를 건설하고 인공위성을 탑재한 로켓을 발사하여 이 목표를 달성하세요. 로켓 격납고의 잠금을 해제하기 위해 고급 기술들을 연구할 필요가 있습니다. 작은 것부터 시작해 자동화로 차근차근 발전하고, 토착 생물로부터 자신을 보호하는 것도 잊지 마세요.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Tai Factorio laisvas žaidimas. Jūsų užduotis - paleisti raketą į kosmosą. Tam reikia pastatyti Raketos šachtą ir paleisti raketą su palydovu. Reikės atrasti pažangias technologijas kad atrakintumėte Raketos šachtą. Pradėkite palengva, tobulinkite automatiką ir neužmirškite apsisaugoti nuo vietinių.
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
msg-intro=Šis ir Factorio brīvais režīms. Tavs uzdevums ir palaist raķeti kosmosā. Lai to izdarītu, tev vajadzēs uzbūvēt palaišanas platformu un palaist raķeti ar satelītu. Tev būs jāizpēta sarežģītas tehnoloģijas, lai atbloķētu palaišanas platformu. Sāc no mazumiņa, strādā un neaizmirsti pasargāt sevi no šīs planētas iemītniekiem...
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Dit is Factorio vrij spel. Je taak is om een raket te lanceren in de ruimte. Bouw hiervoor een raket-silo en lanceer een raket met een satelliet. Je zult geavanceerde technologieën moeten onderzoeken om de raket-silo vrij te spelen. Begin op kleine schaal, breidt uit en automatiseer je fabriek en vergeet niet jezelf te beschermen tegen de inheemse bewoners.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Dette er Factorio frispilling. Din oppgave er å sende en rakett til verdensrommet. Bygg en rakettsilo og send opp en rakett med en satellitt. Du blir nødt til å utforske avanserte teknologier for å få tilgang til rakettsiloen. Begynn i det små, jobb deg oppover med hjelp av automatisering, men ikke glem å beskytte deg mot de innfødte.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=To jest swobodna rozgrywka Factorio. Twoim zadaniem jest wysłanie rakiety w kosmos. Skonstruuj silos rakietowy i wystrzel z niego rakietę z satelitą. Żeby odblokować silos rakietowy będziesz musiał odkryć zaawansowane technologie. Rozpocznij od podstaw, rozwijaj się wykorzystując automatyzację i nie zapomnij o obronie przed rodzimymi mieszkańcami planety.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Este é o modo livre do Factorio. Sua missão é lançar um foguete no espaço. Para fazer isso construa um centro de lançamento de foguetes e lance o foguete com um satélite. Você vai precisar pesquisar tecnologias avançadas para conseguir desbloquear um Centro de lançamento. Comece do começo, continue na automação e não se esqueça de se proteger dos nativos.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Este é o jogo livre Factorio. A tua tarefa é lançar um foguetão para explorar o espaço. Faça isto, construindo um Silo de Foguetão e lançar um foguetão com um satélite. Tu vais precisar de pesquisar tecnologias avançadas para poderes desbloquear o Silo de Foguetão. Começa simples, trabalha e evolui com a automação e não te esqueças de proteger-te dos nativos.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Acesta este modul Factorio freeplay. Sarcina ta este de a lansa o rachetă în spațiu. Fă acest lucru prin construirea unui siloz de rachete și lansarea unei rachete cu satelit. Va trebui să cercetezi tehnologiile avansate pentru a debloca silozul de rachete. Începe încet, avansează cu automatizarea și nu uita să te protejezi de inamici.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Это режим свободной игры Factorio. Ваша задача запустить спутник в космос. Для его запуска потребуется создать ракету и ракетную шахту. Начните с малого, работайте над автоматизацией производства, исследуйте новые технологии и не забывайте о защите от местных форм жизни.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Toto je voľná hra Factorio. Tvojou úlohou je vypustiť raketu do vesmíru. Postav raketové silo a vypusti raketu so satelitom. Pre odomknutie raketového sila budeš musieť vyskúmať pokročilé technológie. Začni v malom, postupuj s automatizáciou a nezabnudni sa chrániť pred domácimi.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=To je Factorio freeplay. Vaša naloga je poslati raketo v vesolje. To storite tako, da zgradite silos za raketo in jo skupaj s satelitom izstrelite v vesolje. Raziskati bo potrebno napredne tehnologije, da bi lahko odklenili Raketni silos. Začnete z majhnim, nato nadgradite vse s pomočjo avtomatizacije in ne pozabite, zaščitite se pred domorodci.
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
msg-intro=Ово је Factorio freeplay. Твој задатак је да лансираш ракету са сателитом у свемир. Уради ово грађом ракетног силоса. Треба да истражиш пуно напредних технологија да би откључао ракетни силос. Почни мало, а онда напредуј са аутоматизацијом и не заборави да се заштитиш од ванземаљаца.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Det här är Factorio frispel. Ditt uppdrag är att avfyra en raket ut i rymden. Gör det genom att tillverka en raket silo och avfyra en raket med en satellit. Du kommer att behöva forska fram avancerade teknologier för att kunna låsa upp raket silon. Börja smått, jobba dig uppåt med hjälp av automatisering och glöm inte att försvara dig själv mot infödingarna.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=นี้คือโหมดอิสระของ Factorio ภารกิจของคุณคือส่งจรวดสู่อวกาศ ซึ่งทำได้โดยการสร้างไซโลจรวดและส่งจรวดขึ้นไปพร้อมกับดาวเทียม คุณต้องวิจัยค้นคว้าเทคโนโลยีขั้นสูงเพื่อปลดล็อคไซโลจรวด เริ่มจากเล็กๆไปเรื่อยๆด้วยระบบอัตโนมัติและอย่าลืมป้องกันตัวเองจากคนพื้นเมืองด้วย
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Bu Factorio'nun serbest oynanışı. Görevin roketleri uzaya yollamak. Bunu Roket Silosu yaparak ve uyduyla roket yollayarak yapacaksın. Roket Silo'sunu açmak için gelişmiş teknolojileri sırayla açmalısın. Yavaş başla, otomatikleş ve yerlilerden kendini korumayı untuma.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Це один з типів гри в Factorio - "Вільна гра". Ваше завдання - запустити ракету у космічний простір. Зробіть це, побудувавши ракетну шахту та запустивши ракету зі супутником. Вам потрібно дослідити передові технології, щоб мати змогу побудувати ракетну шахту. Починайте з малого, розвивайтеся за допомогою автоматизації, але не забувайте і про оборону!
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Đầy là chế độ freeplay của Factorio. Nhiệm cụ của bạn là phóng một chiếc tên lửa lên vũ trụ. Hãy xây dựng một bệ phóng tên lửa để phóng một chiếc tên lửa kèm theo vệ tinh. Bạn sẽ cần phải nghiên cứu nhiều công nghệ tiên tiến để có thể xây dựng bệ phóng tên lửa. Bắt đầu từ quy mô nhỏ, phát triển dần dần bằng tự động hóa, và đừng quên bảo vệ bản thân mình khỏi những sinh vật sinh sống ở đây.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=欢迎来到异星工厂标准模式,你的目标是要把火箭发射到太空。为此你需要研发一系列高等科技,解锁并修建火箭发射井,直到把携带有卫星的火箭发射出去。从小作坊生产起步,逐渐建立起属于自己的自动化大工厂吧!同时,别忘了防范异星虫子的骚扰 ...
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=這是異星工廠的自由模式。你的任務是發射火箭,探索外太空。請建造一個火箭發射井,並發射攜帶著衛星的火箭。你會需要研究大量的進階科技以解鎖火箭發射井。從頭開始,靠著自動化科技的協助向目標邁進時,也別忘記從外星住民手的中保護你自己啊!
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 154 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;"><path d="M105.009,19.309l-3.12,0l-1.68,-1.68l0,-10.293l2.64,-2.64c2.029,0 7.281,0 9.996,0l1.68,1.68l0,6.2l-0.96,0.96l-7.596,-0.003l0,4.816l-0.96,0.953l0,0.007Zm6.876,0l-3.12,0l0,-3.944l0.96,-0.96l4.8,0l0,2.212l-2.64,2.692Zm-63.405,0l-9.917,0l-1.68,-1.68l0,-10.293l2.64,-2.64l3.118,0l0.002,9.827l2.718,0l0.002,-8.867l0.96,-0.96l3.117,0l1.68,1.68l0,10.293l-2.64,2.64Zm-15.652,0l-9.918,0l-1.68,-1.68l0,-10.293l2.64,-2.64l3.118,0l0.002,9.827l8.478,0.009l0,2.137l-2.64,2.64Zm-15.712,0l-9.996,0l-1.68,-1.68l0,-10.293l2.64,-2.64c2.029,0 7.281,0 9.996,0l1.68,1.68c0,1.028 0,2.147 0,2.147l-0.96,0.942l0,0.018l-7.596,0l0,5.04l8.556,0l0,2.146l-2.64,2.64Zm47.185,0l-9.997,0l-1.68,-1.68l0,-2.142l0.96,-0.96l7.597,-0.002l0,-0.993l-8.557,0l0,-6.196l1.68,-1.68l0,0l0.96,-0.96c2.029,0 7.282,0 9.997,0l1.68,1.68c0,1.028 0,2.147 0,2.147l-0.96,0.96l-7.597,0.002l0,0.957l8.557,0l0,6.227l-0.96,0.96l0,0l-1.68,1.68Zm12.444,0l-3.12,0l-1.68,-1.68l0,-6.216l0.96,-0.96l6.48,0l0,6.216l-2.64,2.64Zm19.325,0l-9.996,0l-1.68,-1.68l0,-10.293l2.64,-2.64c2.029,0 7.281,0 9.996,0l1.68,1.68c0,1.028 0,2.147 0,2.147l-1.086,0.96l-7.47,0l0,0.959l5.841,0l0,2.13l-0.96,0.96l-4.881,0l0,0.991l8.556,0l0,2.146l-2.64,2.64Zm20.065,-7.896l0.96,-0.96l6.481,0l0,5.953l-2.64,2.903l-3.121,0l-1.68,-1.943l0,-5.953Zm21.411,7.896l-2.872,0l0,-1.178c-1.382,-0.123 -2.749,-0.431 -4.028,-0.977l-0.056,-0.024l-1.307,0.804l-2.03,-1.25l1.277,-0.786c-0.81,-0.666 -1.487,-1.522 -1.761,-2.534l-1.749,0l0,-1.768l0.96,-0.96l1.141,0c0.332,-0.616 0.809,-1.156 1.35,-1.61l-1.198,-0.78l0.94,-0.93l2.03,-1.25l1.227,0.755c0.949,-0.424 2.033,-0.741 3.204,-0.922l0,-0.248l0.013,0l0.947,-0.96l2.872,0l0,1.116c1.505,0.156 2.889,0.535 4.059,1.079l1.332,-0.82l2.03,1.25l-1.12,1.059c0.701,0.671 1.181,1.437 1.377,2.261l1.976,0l0,1.768l-0.96,0.962l-0.002,-0.002l-1.327,0c-0.228,0.624 -0.547,1.236 -1.053,1.681l1.109,0.679l-0.968,0.978l-0.015,-0.004l-2.007,1.236l-1.411,-0.868c-0.958,0.432 -1.978,0.728 -3.02,0.908l0,0.375l-0.96,0.93l0,0.03Zm-15.17,-7.656l-6.001,0l0,5.624l1.55,1.792l2.901,0l1.55,-1.792l0,-5.624Zm-44.191,0l-6,0l0,5.877l1.54,1.539l2.921,0l1.539,-1.539l0,-5.877Zm35.14,3.952l-4.32,0l0,3.464l2.778,0l1.542,-1.59l0,-1.874Zm-96.308,-9.709l-9.798,0l-1.539,1.54l0,10.094l1.539,1.539l9.798,0l1.539,-1.539l0,-1.807l-8.556,0l0,-6.48l8.556,0l0,-1.807l-1.539,-1.54Zm8.771,0l-2.778,0l-1.54,1.54l0,10.094l1.54,1.539l9.719,0l1.539,-1.539l0,-1.799l-8.478,-0.008l-0.002,-9.827Zm15.653,0l-2.779,0l-1.539,1.54l0,10.094l1.539,1.539l9.719,0l1.539,-1.539l0,-10.094l-1.539,-1.54l-2.778,0l-0.003,9.827l-4.157,0l-0.002,-9.827Zm54.53,0l-9.798,0l-1.539,1.54l0,10.094l1.539,1.539l9.798,0l1.539,-1.539l0,-1.807l-8.556,0l0,-2.431l5.841,0l0,-1.65l-5.841,0l0,-2.399l8.556,0l0,-1.807l-1.539,-1.54Zm-31.77,0l-9.797,0l-0.82,0.82l-0.72,0.72l0,5.856l8.557,0l0,2.432l-8.557,0.003l0,1.803l1.54,1.539l9.797,0l1.54,-1.539l0,-5.888l-8.557,0l0,-2.396l8.557,-0.003l0,-1.807l-1.54,-1.54Zm47.585,0l-9.798,0l-1.539,1.54l0,10.094l1.539,1.539l2.781,0l0,-5.777l8.556,0.004l0,-5.86l-1.539,-1.54Zm28.324,7.453c0.327,-0.363 0.421,-0.95 0.212,-1.53c-0.459,-1.279 -2.339,-2.021 -4.278,-2.122c-0.058,0 -0.058,0 -0.115,0c-2.792,0.201 -3.386,1.31 -3.433,1.781c-0.151,1.505 1.994,2.74 4.45,2.74c1.269,0 2.7,-0.357 3.164,-0.869Zm-31.101,-1.707l0,-2.398l-4.24,0.002l0,2.396l4.24,0Zm-3.04,-2.157l0,0.957l2.8,0l0,-0.958l-2.8,0.001Zm-24.048,0.012l-13.471,0l0,-2.161l2.64,-2.64l10.111,0l1.68,1.68c0,1.034 0,2.161 0,2.161l-0.96,0.96Zm40.606,-0.048l-6.392,0l0,-2.113l2.64,-2.64l4.801,-0.005l0,3.798l-1.049,0.96Zm-42.386,-3.553l-9.912,0l-1.539,1.54l0,1.821l12.991,0l0,-1.821l-1.54,-1.54Zm37.774,0l-1.54,1.54l0,1.773l6.001,0l0,-3.318c-1.487,0.002 -2.974,0.003 -4.461,0.005Z" style="fill:#141414;"/><path d="M73.625,19.309l3.12,0l-3.12,0Zm47.201,-0.24l-2.901,0l-1.55,-1.792l0,-5.624l6.001,0l0,5.624l-1.55,1.792Zm-44.18,0l-2.921,0l-1.54,-1.539l0,-5.877l6,0l0,5.877l-1.539,1.539Zm60.66,0l-2.392,0l0,-1.157l-0.288,-0.026c-1.392,-0.131 -2.761,-0.465 -4.055,-1.026l-1.288,0.792l-1.572,-0.968l1.175,-0.724l0.005,-0.076l-0.25,-0.205c-0.598,-0.5 -1.12,-1.098 -1.438,-1.808c-0.159,-0.355 -0.249,-0.743 -0.25,-0.747l-1.693,0l0,-1.288l1.676,0l0.056,-0.226c0.27,-1.047 1.011,-1.878 1.84,-2.574l-0.005,-0.072l-1.116,-0.688l1.572,-0.968l1.208,0.743l0.228,-0.1c1.335,-0.574 2.76,-0.86 4.195,-0.988l0,-1.072l2.392,0l0,1.092l0.284,0.03c1.394,0.155 2.76,0.518 4.031,1.105l1.316,-0.81l1.572,0.968l-1.259,0.776l0.236,0.276c0.559,0.497 1.039,1.085 1.324,1.77c0.148,0.356 0.225,0.734 0.226,0.738l1.924,0l0,1.288l-1.941,0l-0.059,0.219c-0.367,1.3 -1.768,2.53 -1.768,2.53l1.317,0.811l-1.572,0.968l-1.394,-0.858lLine truncated
|
||||
|
After Width: | Height: | Size: 6.3 KiB |
@@ -76,17 +76,6 @@ describe('Master server endpoint testing', function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
it("sends some HTML when accessing /remoteMap",function(done){
|
||||
this.timeout(6000);
|
||||
request(app).get("/remoteMap").end(function(err,res){
|
||||
assert.equal(res.statusCode, 200);
|
||||
validateHTML(res.text).then(result => {
|
||||
// there should be 1 error regarding complaining about me using ES6 modules before they are fully supported
|
||||
assert(result.messages.length === 2, "there are HTML errors on the page, please fix: "+JSON.stringify(result.messages));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
let persistentMaster = request(app);
|
||||
describe("#POST /api/place", function(){
|
||||
|
||||
Binary file not shown.
+9
@@ -0,0 +1,9 @@
|
||||
module.exports = async function checkHotpatchInstallation(messageInterface){
|
||||
let yn = await messageInterface("/silent-command if remote.interfaces['hotpatch'] then rcon.print('true') else rcon.print('false') end");
|
||||
yn = yn.replace(/(\r\n\t|\n|\r\t)/gm, "");
|
||||
if(yn == "true"){
|
||||
return true;
|
||||
} else if(yn == "false"){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
const fs = require("fs-extra");
|
||||
|
||||
const getLua = require("./getLua.js");
|
||||
|
||||
let commandCache;
|
||||
module.exports = async function getCommand(file, COMPRESS = true){
|
||||
commandCache = commandCache || {};
|
||||
if(!commandCache[file]){
|
||||
commandCache[file] = await getLua(file);
|
||||
return commandCache[file];
|
||||
} else if(typeof commandCache[file] == "string"){
|
||||
return commandCache[file];
|
||||
} else {
|
||||
throw new Error("Command not found");
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
const needle = require("needle");
|
||||
|
||||
module.exports = function(config){
|
||||
const instances = [];
|
||||
|
||||
if(config){
|
||||
return function getInstanceName(instanceID){
|
||||
return new Promise((resolve, reject) => {
|
||||
let instance = instances[instanceID];
|
||||
if(!instance){
|
||||
needle.get(config.masterIP+":"+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")); reject(new Error())}
|
||||
try {
|
||||
for (let index in response.body)
|
||||
instances[index] = response.body[index].instanceName;
|
||||
} catch (e){
|
||||
console.log(e);
|
||||
return null;
|
||||
}
|
||||
instance = instances[instanceID]
|
||||
if (!instance) instance = instanceID; //somehow the master doesn't know the instance
|
||||
resolve(instance);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve(instance);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return async function thisAintGonnaWork(){
|
||||
let err = new Error("getInstanceName from _app/clusterTools needs to be called with {config} to work");
|
||||
console.log(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
const fs = require("fs-extra");
|
||||
|
||||
module.exports = function getLua(filePath, COMPRESS_LUA = true){
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(filePath, "utf8", (err, contents) => {
|
||||
if(err){
|
||||
reject(err);
|
||||
} else {
|
||||
// split content into lines
|
||||
contents = contents.split(/\r?\n/);
|
||||
|
||||
// join those lines after making them safe again
|
||||
contents = contents.reduce((acc, val) => {
|
||||
val = val.replace(/\\/g ,'\\\\');
|
||||
// remove leading and trailing spaces
|
||||
val = val.trim();
|
||||
// escape single quotes
|
||||
val = val.replace(/'/g ,'\\\'');
|
||||
|
||||
// remove single line comments
|
||||
let singleLineCommentPosition = val.indexOf("--");
|
||||
let multiLineCommentPosition = val.indexOf("--[[");
|
||||
|
||||
if(multiLineCommentPosition === -1 && singleLineCommentPosition !== -1) {
|
||||
val = val.substr(0, singleLineCommentPosition);
|
||||
}
|
||||
|
||||
return acc + val + '\\n';
|
||||
}, ""); // need the "" or it will not process the first row, potentially leaving a single line comment in that disables the whole code
|
||||
|
||||
// console.log(contents);
|
||||
|
||||
// this takes about 46 ms to minify train_stop_tracking.lua in my tests on an i3 6100u
|
||||
if(COMPRESS_LUA) contents = require("luamin").minify(contents);
|
||||
|
||||
resolve(contents);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
|
||||
Functions frequently needed from different places in the cluster. Ex:
|
||||
* Loading Lua scripts
|
||||
* Checking if hotpatch is installed
|
||||
* Getting an instance's displayname from their ID
|
||||
|
||||
*/
|
||||
|
||||
module.exports = ({config} = {}) => ({
|
||||
/**
|
||||
async function to get Lua from a file, remove comments, minify and return.
|
||||
|
||||
arg1 is the file path
|
||||
arg2 is whether or not to compress the Lua. Makes debugging harder, but reduces bandwidth when many players are online. Recommended for polling commands. Defaults to true.
|
||||
Promise resolves with a string of Lua.
|
||||
*/
|
||||
getLua: require("./getLua.js"),
|
||||
/**
|
||||
async function to check whether hotpatch is installed in a server.
|
||||
|
||||
First argument is the servers async messageInterface for rcon return values
|
||||
Promise resolves with boolean.
|
||||
*/
|
||||
checkHotpatchInstallation: require("./checkHotpatchInstallation.js"),
|
||||
/**
|
||||
async function to get an instances displayname from their instanceID
|
||||
|
||||
arg1 is the instanceID
|
||||
Promise resolves with a string, either the displayName or instanceID if not found.
|
||||
*/
|
||||
getInstanceName: require("./getInstanceName.js")(config),
|
||||
/**
|
||||
async function same as getLua, but using an in memory cache to lower FS operations.
|
||||
|
||||
arg1 is the file path
|
||||
arg2 is whether or not to compress, default true
|
||||
Promise resolves with a string of Lua.
|
||||
*/
|
||||
getCommand: require("./getCommand.js"),
|
||||
})
|
||||
Generated
-9638
File diff suppressed because it is too large.
Load diff
+26
-12
@@ -10,7 +10,10 @@
|
||||
"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"
|
||||
"install": "node lib/npmPostinstall.js",
|
||||
"bundle-dependencies": "bundle-dependencies",
|
||||
"snyk-protect": "snyk protect",
|
||||
"prepublish": "npm run snyk-protect"
|
||||
},
|
||||
"keywords": [
|
||||
"factorio"
|
||||
@@ -21,41 +24,51 @@
|
||||
"app-root-path": "^2.0.1",
|
||||
"as-table": "^1.0.31",
|
||||
"averaged-timeseries": "^1.4.0",
|
||||
"base64url": "^3.0.0",
|
||||
"bcrypt": "^2.0.1",
|
||||
"bcrypt-promise": "^2.0.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"body-parser": "^1.18.2",
|
||||
"codecov": "^2.3.1",
|
||||
"cache-require-paths": "^0.3.0",
|
||||
"cli-interact": "^0.1.9",
|
||||
"codecov": "^3.0.1",
|
||||
"compression": "^1.7.2",
|
||||
"cookie-parser": "^1.4.3",
|
||||
"deepmerge": "^1.5.2",
|
||||
"ejs": "^2.5.7",
|
||||
"express": "^4.16.2",
|
||||
"express-fileupload": "0.0.6",
|
||||
"follow-redirects": "^1.4.1",
|
||||
"fs-extra": "^1.0.0",
|
||||
"express-prometheus-request-metrics": "github:burnash/express-prometheus-request-metrics",
|
||||
"follow-redirects": "^1.5.0",
|
||||
"fs-extra": "^6.0.1",
|
||||
"getmac": "^1.4.0",
|
||||
"hash-files": "^1.1.1",
|
||||
"html5-validator": "^1.0.7",
|
||||
"ini": "^1.3.5",
|
||||
"jsonwebtoken": "^8.2.1",
|
||||
"leveldown": "^1.9.0",
|
||||
"linvodb3": "^3.25.1",
|
||||
"luamin": "^1.0.4",
|
||||
"minimist": "^1.2.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"mkdirp-promise": "^5.0.1",
|
||||
"moment": "^2.20.1",
|
||||
"moment": "^2.22.2",
|
||||
"ncp": "^2.0.0",
|
||||
"nedb": "^1.8.0",
|
||||
"needle": "^1.1.0",
|
||||
"needle": "^2.2.4",
|
||||
"node-factorio-api": "^0.3.1-alpha",
|
||||
"node-forge": "^0.7.1",
|
||||
"prom-client": "^10.2.3",
|
||||
"rcon-client": "^3.0.2",
|
||||
"request": "^2.83.0",
|
||||
"right-pad": "^1.0.1",
|
||||
"rmdir-sync": "^1.0.1",
|
||||
"simple-rcon": "^0.3.1",
|
||||
"sanitizer": "^0.1.3",
|
||||
"simple-git": "^1.96.0",
|
||||
"single-line-log": "^1.1.2",
|
||||
"socket.io": "^2.0.4",
|
||||
"socket.io-client": "^2.0.4",
|
||||
"socket.io-prometheus": "^0.2.1",
|
||||
"sync-request": "^4.0.1",
|
||||
"underscore": "^1.8.3"
|
||||
"underscore": "^1.8.3",
|
||||
"snyk": "^1.134.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"documentation": "^5.4.0",
|
||||
@@ -66,5 +79,6 @@
|
||||
"nock": "^9.1.6",
|
||||
"sinon": "^2.4.1",
|
||||
"supertest": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"snyk": true
|
||||
}
|
||||
@@ -11,7 +11,4 @@ module.exports = function(app) {
|
||||
app.get("/nodeDetails", function(req, res) {
|
||||
res.render("nodeDetails");
|
||||
});
|
||||
app.get("/remoteMap", function(req, res) {
|
||||
res.render("remoteMap");
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
const fs = require("fs-extra")
|
||||
const path = require("path")
|
||||
|
||||
module.exports = function(app){
|
||||
app.get("/api/getPictures", async (req, res) => {
|
||||
let pictures = (await fs.readdir(path.join(__dirname, `../../static/pictures`)))
|
||||
.filter(path => path.includes(".png"))
|
||||
.map(name => ({
|
||||
name: name.replace(".png",""),
|
||||
path: `/pictures/${name}`
|
||||
}))
|
||||
|
||||
|
||||
res.send(pictures)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
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=[]
|
||||
|
||||
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)
|
||||
)
|
||||
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]
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
--
|
||||
-- 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
|
||||
@@ -22,12 +24,13 @@
|
||||
-- DEALINGS IN THE SOFTWARE.
|
||||
|
||||
function write_file(data)
|
||||
stringsToWriteNextTick = stringsToWriteNextTick .. data .. '\n'
|
||||
global.tileQueue = global.tileQueue .. 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)
|
||||
@@ -89,7 +92,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)
|
||||
@@ -104,7 +107,7 @@ function on_some_entity_created(event, entp)
|
||||
complain("wtf, on_some_entity_created has nil entity")
|
||||
return
|
||||
end
|
||||
local entityData = ent.name..","..ent.position.x..","..ent.position.y
|
||||
local entityData = "name="..ent.name..",x="..ent.position.x..",y="..ent.position.y
|
||||
if ent.supports_direction then
|
||||
entityData = entityData .. ",rot="..ent.direction
|
||||
end
|
||||
@@ -126,7 +129,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("deleted,"..ent.position.x..","..ent.position.y)
|
||||
write_file("name=deleted,x="..ent.position.x..",y="..ent.position.y)
|
||||
--complain("on_some_entity_deleted: "..ent.name.." at "..ent.position.x..","..ent.position.y)
|
||||
end
|
||||
|
||||
@@ -143,3 +146,22 @@ 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
|
||||
})
|
||||
@@ -3,12 +3,20 @@ const needle = require("needle");
|
||||
|
||||
|
||||
class UPSdisplay {
|
||||
constructor(slaveConfig, msgInterface){
|
||||
constructor(slaveConfig, msgInterface, extras){
|
||||
this.config = slaveConfig;
|
||||
this.messageInterface = msgInterface;
|
||||
this.socket = extras.socket;
|
||||
|
||||
this.historicalTicks = [];
|
||||
setInterval(() => {
|
||||
this.messageInterface("/silent-command game.write_file('UPSdisplay.txt', game.tick, true, 0)");
|
||||
this.messageInterface("/silent-command rcon.print(game.tick)", data => {
|
||||
this.tick = Number(data);
|
||||
this.historicalTicks[this.historicalTicks.length] = {tick:Number(data), timestamp:Date.now()};
|
||||
if(this.historicalTicks.length > 30){
|
||||
this.historicalTicks.shift(); // delete last element in array (position 0);
|
||||
}
|
||||
});
|
||||
},1000);
|
||||
setInterval(() => {
|
||||
if(this.historicalTicks && this.historicalTicks[0]){
|
||||
@@ -17,24 +25,15 @@ class UPSdisplay {
|
||||
let UPS = Math.round(ticksInPeriod / (timePeriod/1000));
|
||||
// console.log("UPS: " + UPS);
|
||||
try{
|
||||
needle.post(this.config.masterIP+':'+this.config.masterPort+'/api/editSlaveMeta', {instanceID: this.config.unique, password: this.config.clientPassword, meta: {UPS:UPS}}, function(err, resp) {
|
||||
needle.post(this.config.masterIP+':'+this.config.masterPort+'/api/editSlaveMeta', {instanceID: this.config.unique, password: this.config.clientPassword, meta: {UPS:UPS, tick:this.tick}}, {headers: {'x-access-token': this.config.masterAuthToken}}, function(err, resp) {
|
||||
// success?
|
||||
});
|
||||
} catch (err){
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
}, 10000);
|
||||
}, 5000);
|
||||
this.messageInterface("/silent-command game.print('UPSdisplay enabled')");
|
||||
}
|
||||
scriptOutput(data){
|
||||
if (data !== null && !isNaN(data)){
|
||||
// console.log("Tick: " + data);
|
||||
this.historicalTicks[this.historicalTicks.length] = {tick:Number(data), timestamp:Date.now()};
|
||||
if(this.historicalTicks.length > 30){
|
||||
this.historicalTicks.shift(); // delete last element in array (position 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
module.exports = UPSdisplay;
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
Clusterio plugin to allow for chat between instances.
|
||||
*/
|
||||
module.exports = {
|
||||
// Name of package. For display somewhere I guess.
|
||||
name: "clusterioMod",
|
||||
version: "1.0.0",
|
||||
binary: "nodePackage",
|
||||
description: "Server side handling of functionality in the clusterio lua mod",
|
||||
masterPlugin: "masterPlugin.js",
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
const needle = require("needle");
|
||||
const fs = require("fs-extra");
|
||||
|
||||
const objectOps = require("./../../lib/objectOps.js");
|
||||
const fileOps = require("_app/fileOps");
|
||||
const stringUtils = require("./../../lib/stringUtils.js");
|
||||
|
||||
const pluginConfig = require("./config");
|
||||
const COMPRESS_LUA = false;
|
||||
|
||||
module.exports = class remoteCommands {
|
||||
constructor(mergedConfig, messageInterface, extras){
|
||||
this.messageInterface = messageInterface;
|
||||
this.config = mergedConfig;
|
||||
this.socket = extras.socket;
|
||||
this.socket.on("hello", () => {
|
||||
|
||||
});
|
||||
const that = this;
|
||||
const instance = process.argv[3];
|
||||
const instancedirectory = this.config.instanceDirectory + '/' + instance;
|
||||
|
||||
const needleOptionsWithTokenAuthHeader = {
|
||||
headers: {
|
||||
'x-access-token': this.config.masterAuthToken
|
||||
},
|
||||
};
|
||||
|
||||
(async ()=>{
|
||||
let hotpatchInstallStatus = await this.checkHotpatchInstallation();
|
||||
this.messageInterface("Hotpach installation status: "+hotpatchInstallStatus);
|
||||
if(hotpatchInstallStatus){
|
||||
// let mainCode = await this.getSafeLua("sharedPlugins/playerManager/lua/playerTracking.lua");
|
||||
// if(mainCode) var returnValue = await messageInterface("/silent-command remote.call('hotpatch', 'update', '"+pluginConfig.name+"', '"+pluginConfig.version+"', '"+mainCode+"')");
|
||||
}
|
||||
})().catch(e => console.log(e));
|
||||
|
||||
// flow/production statistics ------------------------------------------------------------
|
||||
var oldFlowStats = false;
|
||||
var oldTimestamp;
|
||||
var oldFlowStats;
|
||||
setInterval(function(){
|
||||
fs.readFile(instancedirectory + "/script-output/flows.txt", {encoding: "utf8"}, function(err, data) {
|
||||
if(!err && data) {
|
||||
let timestamp = Date.now();
|
||||
data = data.split("\n");
|
||||
let flowStats = [];
|
||||
for(let i = 0; i < data.length; i++) {
|
||||
// try catch to remove any invalid json
|
||||
try{
|
||||
flowStats[flowStats.length] = JSON.parse(data[i]);
|
||||
} catch (e) {
|
||||
// console.log(" invalid json: " + i);
|
||||
// some lines of JSON are invalid but don't worry, we just filter em out
|
||||
}
|
||||
}
|
||||
// fluids
|
||||
let flowStat1 = flowStats[flowStats.length-1].flows.player.input_counts
|
||||
// items
|
||||
let flowStat2 = flowStats[flowStats.length-2].flows.player.input_counts
|
||||
// merge fluid and item flows
|
||||
let totalFlows = {};
|
||||
for(let key in flowStat1) totalFlows[key] = flowStat1[key];
|
||||
for(let key in flowStat2) totalFlows[key] = flowStat2[key];
|
||||
if(oldFlowStats && totalFlows && oldTimestamp) {
|
||||
let payload = objectOps.deepclone(totalFlows);
|
||||
// change from total reported to per time unit
|
||||
for(let key in oldFlowStats) {
|
||||
// get production per minute
|
||||
payload[key] = Math.floor((payload[key] - oldFlowStats[key])/(timestamp - oldTimestamp)*60000);
|
||||
if(payload[key] < 0) {
|
||||
payload[key] = 0;
|
||||
}
|
||||
}
|
||||
for(let key in payload) {
|
||||
if(payload[key] == '0') {
|
||||
delete payload[key];
|
||||
}
|
||||
}
|
||||
console.log("Recorded flows, copper plate since last time: " + payload["copper-plate"]);
|
||||
needle.post(that.config.masterIP + ":" + that.config.masterPort + '/api/logStats', {timestamp: timestamp, instanceID: that.config.unique, data: payload}, needleOptionsWithTokenAuthHeader, function (err, response, body) {
|
||||
// we did it, keep going
|
||||
});
|
||||
}
|
||||
oldTimestamp = timestamp;
|
||||
oldFlowStats = totalFlows;
|
||||
fs.writeFileSync(instancedirectory + "/script-output/flows.txt", "");
|
||||
}
|
||||
});
|
||||
// we don't need to update stats quickly as that could be expensive
|
||||
}, 60000*5);
|
||||
|
||||
// provide items --------------------------------------------------------------
|
||||
// trigger when something happens to output.txt
|
||||
fs.watch(instancedirectory + "/script-output/output.txt", function (eventType, filename) {
|
||||
// get array of lines in file
|
||||
let items = fs.readFileSync(instancedirectory + "/script-output/output.txt", "utf8").split("\n");
|
||||
// if you found anything, reset the file
|
||||
if (items[0]) {
|
||||
fs.writeFileSync(instancedirectory + "/script-output/output.txt", "");
|
||||
}
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i]) {
|
||||
let g = items[i].split(" ");
|
||||
g[0] = g[0].replace("\u0000", "");
|
||||
// console.log("exporting " + JSON.stringify(g));
|
||||
// send our entity and count to the master for him to keep track of
|
||||
needle.post(that.config.masterIP + ":" + that.config.masterPort + '/api/place', {
|
||||
name: g[0],
|
||||
count: g[1],
|
||||
instanceName: instance, // name of instance
|
||||
instanceID: that.config.unique, // a hash computed from the randomly generated rcon password
|
||||
}, needleOptionsWithTokenAuthHeader, function (err, resp, body) {
|
||||
if(body == "failure") console.error("#### Export failed! Lost: "+g[1]+" "+g[0]);
|
||||
if(that.config.logItemTransfers){
|
||||
if(body == "success") console.log(`Exported ${g[1]} ${g[0]} to master`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// request items --------------------------------------------------------------
|
||||
setInterval(function () {
|
||||
// get array of lines in file
|
||||
let items = fs.readFileSync(instancedirectory + "/script-output/orders.txt", "utf8").split("\n");
|
||||
// if we actually got anything from the file, proceed and reset file
|
||||
if (items[0]) {
|
||||
fs.writeFileSync(instancedirectory + "/script-output/orders.txt", "");
|
||||
// prepare a package of all our requested items in a more transfer friendly format
|
||||
var preparedPackage = {};
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
(function (i) {
|
||||
if (items[i]) {
|
||||
items[i] = items[i].split(" ");
|
||||
items[i][0] = items[i][0].replace("\u0000", "");
|
||||
items[i][0] = items[i][0].replace(",", "");
|
||||
if (preparedPackage[items[i][0]]) {
|
||||
// if we have buffered some already, sum the new items
|
||||
if (typeof Number(preparedPackage[items[i][0]].count) == "number" && typeof Number(items[i][1]) == "number") {
|
||||
preparedPackage[items[i][0]] = {
|
||||
"name": items[i][0],
|
||||
"count": Number(preparedPackage[items[i][0]].count) + Number(items[i][1]),
|
||||
"instanceName":instance,
|
||||
"instanceID":that.config.unique,
|
||||
};
|
||||
// else just add em in without summing
|
||||
} else if (typeof Number(items[i][1]) == "number") {
|
||||
preparedPackage[items[i][0]] = {
|
||||
"name": items[i][0],
|
||||
"count": Number(items[i][1]),
|
||||
"instanceName":instance,
|
||||
"instanceID":that.config.unique,
|
||||
};
|
||||
}
|
||||
// this condition will NEVER be triggered but we know how that goes
|
||||
} else if (typeof Number(items[i][1]) == "number") {
|
||||
preparedPackage[items[i][0]] = {
|
||||
"name": items[i][0],
|
||||
"count": Number(items[i][1]),
|
||||
"instanceName":instance,
|
||||
"instanceID":that.config.unique,
|
||||
};
|
||||
}
|
||||
}
|
||||
})(i);
|
||||
}
|
||||
// request our items, one item at a time
|
||||
for (let i = 0; i < Object.keys(preparedPackage).length; i++) {
|
||||
// console.log(preparedPackage[Object.keys(preparedPackage)[i]]);
|
||||
needle.post(that.config.masterIP + ":" + that.config.masterPort + '/api/remove', preparedPackage[Object.keys(preparedPackage)[i]], needleOptionsWithTokenAuthHeader, function (err, response, body) {
|
||||
if (response && response.body && typeof response.body == "object") {
|
||||
// buffer confirmed orders
|
||||
confirmedOrders[confirmedOrders.length] = {name:response.body.name,count:response.body.count}
|
||||
if(that.config.logItemTransfers){
|
||||
console.log(`Imported ${response.body.count} ${response.body.name} from master`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// if we got some confirmed orders
|
||||
// console.log("Importing " + confirmedOrders.length + " items! " + JSON.stringify(confirmedOrders));
|
||||
//if (!(confirmedOrders.length>0)){return;}
|
||||
let cmd="local t={";
|
||||
for(let i=0;i<confirmedOrders.length;i++)
|
||||
{
|
||||
cmd+='["'+confirmedOrders[i].name+'"]='+confirmedOrders[i].count+',';
|
||||
}
|
||||
if (!(cmd==="local t={")){
|
||||
that.messageInterface("/silent-command remote.call('clusterio', 'runcode', '"+cmd.slice(0, -1)+"}"+ " for k, item in pairs(t) do GiveItemsToStorage(k, item) end')");
|
||||
}
|
||||
confirmedOrders=[];
|
||||
}
|
||||
}, 1000);
|
||||
// COMBINATOR SIGNALS ---------------------------------------------------------
|
||||
// get inventory from Master and RCON it to our slave
|
||||
setInterval(() => {
|
||||
needle.get(that.config.masterIP + ":" + that.config.masterPort + '/api/inventory', function (err, response, body) {
|
||||
if(err){
|
||||
console.error("Unable to get JSON master/api/inventory, master might be unaccessible");
|
||||
} else if (response && response.body) {
|
||||
// Take the inventory we (hopefully) got and turn it into the format LUA accepts
|
||||
if(Buffer.isBuffer(response.body)) {console.log(response.body.toString("utf-8")); throw new Error();} // We are probably contacting the wrong webserver
|
||||
try {
|
||||
var inventory = JSON.parse(response.body);
|
||||
var inventoryFrame = {};
|
||||
for (let i = 0; i < inventory.length; i++) {
|
||||
inventoryFrame[inventory[i].name] = Number(inventory[i].count);
|
||||
if(inventoryFrame[inventory[i].name] >= Math.pow(2, 31)){
|
||||
inventoryFrame[inventory[i].name] = Math.pow(2, 30); // set it waaay lower, 31 -1 would probably suffice
|
||||
}
|
||||
}
|
||||
inventoryFrame["signal-unixtime"] = Math.floor(Date.now()/1000);
|
||||
// console.log("RCONing inventory! " + JSON.stringify(inventoryFrame));
|
||||
let first = true;
|
||||
let cmd="local s={";
|
||||
for (let key in inventoryFrame)
|
||||
{
|
||||
cmd+='["'+key+'"]='+inventoryFrame[key]+",";
|
||||
}
|
||||
if (!(cmd==="local s={")){
|
||||
that.messageInterface("/silent-command remote.call('clusterio', 'runcode', '"+(first ? 'global.ticksSinceMasterPinged=0 ':'')+cmd.slice(0, -1)+"}"+ " for name,count in pairs(s) do global.invdata[name]=count end')");
|
||||
}
|
||||
that.messageInterface("/silent-command remote.call('clusterio', 'runcode', 'UpdateInvCombinators()')");
|
||||
} catch (e){
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 1550);
|
||||
// Make sure world has its worldID
|
||||
setTimeout(function(){
|
||||
messageInterface("/silent-command remote.call('clusterio','setWorldID',"+that.config.unique+")")
|
||||
}, 20000);
|
||||
/* REMOTE SIGNALLING
|
||||
* send any signals the slave has been told to send
|
||||
* Fetch combinator signals from the server
|
||||
*/
|
||||
this.socket.on("processCombinatorSignal", circuitFrameWithMeta => {
|
||||
if(circuitFrameWithMeta && typeof circuitFrameWithMeta == "object" && circuitFrameWithMeta.frame && Array.isArray(circuitFrameWithMeta.frame)){
|
||||
messageInterface("/silent-command remote.call('clusterio', 'receiveFrame', '"+JSON.stringify(circuitFrameWithMeta.frame)+"')");
|
||||
}
|
||||
});
|
||||
// get outbound frames from file and send to master
|
||||
// get array of lines in file, each line should correspond to a JSON encoded frame
|
||||
let signals = fs.readFileSync(instancedirectory + "/script-output/txbuffer.txt", "utf8").split("\n");
|
||||
// if we actually got anything from the file, proceed and reset file
|
||||
let readingTxBufferSoon = false;
|
||||
let txBufferClearCounter = 0;
|
||||
fs.watch(instancedirectory + "/script-output/txbuffer.txt", "utf-8", (eventType, filename) => {
|
||||
if(!readingTxBufferSoon){ // use a 100ms delay to avoid messing with rapid sequential writes from factorio (I think that might be a problem maybe?)
|
||||
readingTxBufferSoon = true;
|
||||
setTimeout(()=>{
|
||||
txBufferClearCounter++;
|
||||
fs.readFile(instancedirectory + "/script-output/txbuffer.txt", "utf-8", (err, signals) => {
|
||||
signals = signals.split("\n");
|
||||
if (signals[0]) {
|
||||
//if(txBufferClearCounter > 500){
|
||||
fs.writeFileSync(instancedirectory + "/script-output/txbuffer.txt", "");
|
||||
// txBufferClearCounter = 0;
|
||||
//}
|
||||
|
||||
// loop through all our frames
|
||||
for (let i = 0; i < signals.length; i++) {
|
||||
if (signals[i] && objectOps.isJSON(signals[i])) {
|
||||
// signals[i] is a JSON array called a "frame" of signals. We timestamp it for storage on master
|
||||
// then we unpack and RCON in this.frame to the game later.
|
||||
let framepart = JSON.parse(signals[i]);
|
||||
let doneframe = {
|
||||
time: Date.now(),
|
||||
frame: framepart, // thats our array of objects(single signals);
|
||||
}
|
||||
// send to master using socket.io, opened at the top of instanceManagement()
|
||||
this.socket.emit("combinatorSignal", doneframe);
|
||||
} else {
|
||||
// console.log("Invalid jsony: "+typeof signals[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
readingTxBufferSoon = false;
|
||||
},100);
|
||||
}
|
||||
});
|
||||
}
|
||||
async getCommand(file){
|
||||
this.commandCache = this.commandCache || {};
|
||||
if(!this.commandCache[file]){
|
||||
try{
|
||||
let command = (await fs.readFile(file)).toString();
|
||||
this.commandCache[file] = command.replace(/(\r\n\t|\n|\r\t)/gm, " "); // remove newlines
|
||||
return this.commandCache[file];
|
||||
} catch(e){
|
||||
console.log("Unable to get command from file!");
|
||||
console.log(e)
|
||||
}
|
||||
} else if(typeof this.commandCache[file] == "string"){
|
||||
return this.commandCache[file];
|
||||
} else {
|
||||
throw new Error("Command not found");
|
||||
}
|
||||
}
|
||||
async factorioOutput(data){
|
||||
|
||||
}
|
||||
getInstanceName(instanceID){
|
||||
return new Promise((resolve, reject) => {
|
||||
let instance = this.instances[instanceID];
|
||||
if(!instance){
|
||||
needle.get(this.config.masterIP+":"+this.config.masterPort+ '/api/slaves', (err, response) => {
|
||||
if(err || response.statusCode != 200) {
|
||||
console.log("Unable to get JSON master/api/slaves, master might be unaccessible");
|
||||
} else if (response && response.body) {
|
||||
if(Buffer.isBuffer(response.body)) {console.log(response.body.toString("utf-8")); throw new Error();}
|
||||
try {
|
||||
for (let index in response.body)
|
||||
this.instances[index] = response.body[index].instanceName;
|
||||
} catch (e){
|
||||
console.log(e);
|
||||
return null;
|
||||
}
|
||||
instance = this.instances[instanceID]
|
||||
if (!instance) instance = instanceID; //somehow the master doesn't know the instance
|
||||
resolve(instance);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve(instance);
|
||||
}
|
||||
});
|
||||
}
|
||||
async getSafeLua(filePath){
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(filePath, "utf8", (err, contents) => {
|
||||
if(err){
|
||||
reject(err);
|
||||
} else {
|
||||
// split content into lines
|
||||
contents = contents.split(/\r?\n/);
|
||||
|
||||
// join those lines after making them safe again
|
||||
contents = contents.reduce((acc, val) => {
|
||||
val = val.replace(/\\/g ,'\\\\');
|
||||
// remove leading and trailing spaces
|
||||
val = val.trim();
|
||||
// escape single quotes
|
||||
val = val.replace(/'/g ,'\\\'');
|
||||
|
||||
// remove single line comments
|
||||
let singleLineCommentPosition = val.indexOf("--");
|
||||
let multiLineCommentPosition = val.indexOf("--[[");
|
||||
|
||||
if(multiLineCommentPosition === -1 && singleLineCommentPosition !== -1) {
|
||||
val = val.substr(0, singleLineCommentPosition);
|
||||
}
|
||||
|
||||
return acc + val + '\\n';
|
||||
}, ""); // need the "" or it will not process the first row, potentially leaving a single line comment in that disables the whole code
|
||||
|
||||
// console.log(contents);
|
||||
|
||||
// this takes about 46 ms to minify train_stop_tracking.lua in my tests on an i3
|
||||
if(COMPRESS_LUA) contents = require("luamin").minify(contents);
|
||||
|
||||
resolve(contents);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
async checkHotpatchInstallation(){
|
||||
let yn = await this.messageInterface("/silent-command if remote.interfaces['hotpatch'] then rcon.print('true') else rcon.print('false') end");
|
||||
yn = yn.replace(/(\r\n\t|\n|\r\t)/gm, "");
|
||||
if(yn == "true"){
|
||||
return true;
|
||||
} else if(yn == "false"){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
async function sleep(s){
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(resolve, s*1000);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
const fs = require("fs-extra");
|
||||
const path = require("path");
|
||||
const Express = require("express");
|
||||
const ejs = require("ejs");
|
||||
|
||||
class masterPlugin {
|
||||
constructor({config, pluginConfig, pluginPath, socketio, express}){
|
||||
this.config = config;
|
||||
this.pluginConfig = pluginConfig;
|
||||
this.pluginPath = pluginPath;
|
||||
this.io = socketio;
|
||||
this.app = express;
|
||||
|
||||
// load databases
|
||||
this.items = getDatabaseSync("database/items.json");
|
||||
|
||||
this.clients = {};
|
||||
this.slaves = {};
|
||||
|
||||
// initialize web API
|
||||
// require("./js/api-endpoints.js")(this);
|
||||
|
||||
// expose UI elements embedded in the master
|
||||
// this.ui = require("./js/ui.js").ui;
|
||||
|
||||
this.io.on("connection", socket => {
|
||||
|
||||
});
|
||||
}
|
||||
async broadcastCommand(command){
|
||||
let returnValues = [];
|
||||
for(let instanceID in this.slaves){
|
||||
let slave = this.slaves[instanceID];
|
||||
slave.emit("runCommand", {
|
||||
// commandID:Math.random(),
|
||||
command,
|
||||
});
|
||||
};
|
||||
return returnValues;
|
||||
}
|
||||
findInArray(key, value, array){
|
||||
let indexes = [];
|
||||
for(let i in array){
|
||||
if(array[i][key] && array[i][key] === value) indexes.push(i);
|
||||
}
|
||||
return indexes;
|
||||
}
|
||||
async onExit(){
|
||||
// await saveDatabase("database/items.json", this.items);
|
||||
return;
|
||||
}
|
||||
}
|
||||
module.exports = masterPlugin;
|
||||
|
||||
function arrayRemoveDuplicates(array){
|
||||
let newArray = [];
|
||||
array.forEach(value => {
|
||||
if(!newArray.includes(value)) newArray.push(value);
|
||||
});
|
||||
return newArray;
|
||||
}
|
||||
function getDatabaseSync(path){
|
||||
let db;
|
||||
try {
|
||||
db = JSON.parse(fs.readFileSync(path, "utf8"));
|
||||
} catch(e){
|
||||
db = {};
|
||||
}
|
||||
return db;
|
||||
}
|
||||
async function saveDatabase(path, database){
|
||||
if(!path){
|
||||
throw new Error("No path provided!");
|
||||
} else if(!database){
|
||||
throw new Error("No database provided!");
|
||||
} else {
|
||||
try {
|
||||
await fs.writeFile(path, JSON.stringify(database, null, 4));
|
||||
} catch(e){
|
||||
throw new Error("Unable to write to database! "+path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
Clusterio plugin to allow for chat between instances.
|
||||
*/
|
||||
module.exports = {
|
||||
// Name of package. For display somewhere I guess.
|
||||
name: "globalChat",
|
||||
version: "1.0.0",
|
||||
binary: "nodePackage",
|
||||
description: "Clusterio plugin to allow for chat between instances.",
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
const needle = require("needle");
|
||||
|
||||
module.exports = class remoteCommands {
|
||||
constructor(mergedConfig, messageInterface, extras){
|
||||
this.messageInterface = messageInterface;
|
||||
this.config = mergedConfig;
|
||||
this.socket = extras.socket;
|
||||
this.instances = {};
|
||||
|
||||
this.socket.on("hello", () => this.socket.emit("registerChatReciever"));
|
||||
|
||||
this.socket.on("gameChat", async data => {
|
||||
if(data.instanceID && data.instanceID != this.config.unique.toString()){
|
||||
if(data.data.includes("[CHAT]")
|
||||
// if this isn't used as an info command or something (reduce spam)
|
||||
&& !data.data.includes("!info")
|
||||
// check if we are allowed to cross chat at all, default to true
|
||||
&& (mergedConfig.enableCrossServerShout == undefined || mergedConfig.enableCrossServerShout)
|
||||
// check if we are supposed to relay this message
|
||||
&& (mergedConfig.mirrorAllChat || data.data.toLowerCase().includes("/shout") || data.data.toLowerCase().includes("!shout"))){
|
||||
let words = data.data.trim().split(" ");
|
||||
words.shift();
|
||||
words.shift();
|
||||
words.shift();
|
||||
let message = words.join(" ").replace(/[";]/g, " ").replace(/[']/g,"").replace("/shout ", "").replace("!shout", "");
|
||||
|
||||
let instance = await this.getInstanceName(data.instanceID);
|
||||
|
||||
this.messageInterface("/silent-command game.print('[" + instance + "] " + message + "')");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
async factorioOutput(data){
|
||||
try{
|
||||
this.socket.emit("gameChat", {
|
||||
instanceID: this.config.unique.toString(),
|
||||
data,
|
||||
});
|
||||
if(data.includes("[CHAT]") && data.includes("!info")){
|
||||
let infoMessage = [
|
||||
"=== Server info ===",
|
||||
"Name: "+await this.getInstanceName(this.config.unique)+" ("+this.config.unique+")",
|
||||
];
|
||||
infoMessage.forEach((line, i) => {
|
||||
setTimeout(()=>this.messageInterface("/silent-command game.print('"+line+"')"),i*200);
|
||||
});
|
||||
}
|
||||
}catch(e){console.log(e)}
|
||||
}
|
||||
getInstanceName(instanceID){
|
||||
return new Promise((resolve, reject) => {
|
||||
let instance = this.instances[instanceID];
|
||||
if(!instance){
|
||||
needle.get(this.config.masterIP+":"+this.config.masterPort+ '/api/slaves', (err, response) => {
|
||||
if(err || response.statusCode != 200) {
|
||||
console.log("Unable to get JSON master/api/slaves, master might be unaccessible");
|
||||
} else if (response && response.body) {
|
||||
if(Buffer.isBuffer(response.body)) {console.log(response.body.toString("utf-8")); throw new Error();}
|
||||
try {
|
||||
for (let index in response.body)
|
||||
this.instances[index] = response.body[index].instanceName;
|
||||
} catch (e){
|
||||
console.log(e);
|
||||
return null;
|
||||
}
|
||||
instance = this.instances[instanceID]
|
||||
if (!instance) instance = instanceID; //somehow the master doesn't know the instance
|
||||
resolve(instance);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve(instance);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ class inventoryImports {
|
||||
this.messageInterface = messageInterface;
|
||||
messageInterface("/silent-command game.print('InventoryImports enabled')");
|
||||
setInterval(function(){
|
||||
messageInterface("Polling inventories\n")
|
||||
// messageInterface("Polling inventories\n")
|
||||
messageInterface(functions.pollInventories(pluginConfig.scriptOutputFileSubscription));
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ function handleInventory(json, config, callback){
|
||||
}
|
||||
//console.log("Requesting: "+JSON.stringify(request));
|
||||
// request the difference from master
|
||||
needle.post(config.masterIP + ":" + config.masterPort + '/api/remove', request, {json:true}, function (err, response, body) {
|
||||
needle.post(config.masterIP + ":" + config.masterPort + '/api/remove', request, {json:true,headers: {'x-access-token': config.masterAuthToken}}, function (err, response, body) {
|
||||
if (!err && response && response.body && (typeof response.body == "object" || isJson(response.body))) {
|
||||
// confirmed orders, already removed from master inventory
|
||||
//response.body.name: response.body.count
|
||||
@@ -103,7 +103,7 @@ function handleInventory(json, config, callback){
|
||||
for(let i = 0; i < stackArray.length; i++){
|
||||
if(stackArray[i].count > 0){
|
||||
// console.log("Returning overflow: " + JSON.stringify(stackArray[i]));
|
||||
needle.post(config.masterIP + ":" + config.masterPort + '/api/place', stackArray[i], function (err, resp, body) {
|
||||
needle.post(config.masterIP + ":" + config.masterPort + '/api/place', stackArray[i], {headers: {'x-access-token': config.masterAuthToken}}, function (err, resp, body) {
|
||||
// console.log(body);
|
||||
});
|
||||
}
|
||||
@@ -141,7 +141,7 @@ function pollInventories(outputFile){
|
||||
//console.log("/silent-command "+'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.write_file("'+pluginConfig.scriptOutputFileSubscription+'",e)')
|
||||
// console.log("/silent-command "+'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.write_file("'+pluginConfig.scriptOutputFileSubscription+'",a)')
|
||||
//return "/silent-command "+'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.write_file("'+outputFile+'",a)'
|
||||
return '/silent-command '+'local a='+"'"+'{"players":{'+"'"+'local b=false;for c,d in pairs(game.players)do if d.connected and d.character 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)'
|
||||
return '/silent-command '+'local a='+"'"+'{"players":{'+"'"+'local b=false;for c,d in pairs(game.players)do if d.connected and d.character 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={}local z={}for j,k in pairs(e)do if z[j] ~= nil then z[j] = z[j] + k else z[j] = k end end for j,k in pairs(f)do if z[j] ~= nil then z[j] = z[j] + k else z[j] = k end end 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(z)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)'
|
||||
/*
|
||||
needle.post(config.masterIP+':'+config.masterPort+'/api/editSlaveMeta', {instanceID: config.unique, password: config.clientPassword, meta: {UPS:UPS}}, function(err, resp) {
|
||||
// success?
|
||||
|
||||
@@ -22,4 +22,4 @@ module.exports = class remoteCommands {
|
||||
scriptOutput(data){
|
||||
// we don't return anything yet
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
var mkdirp = require("mkdirp-promise");
|
||||
var mkdirpSync = require("mkdirp").sync;
|
||||
var fs = require("fs");
|
||||
var events = require("events");
|
||||
|
||||
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);
|
||||
|
||||
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?
|
||||
*/
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
var assert = require("assert");
|
||||
|
||||
var chunkStore = require("./chunkStore.js");
|
||||
|
||||
describe("remoteMap/chunkStore.js", () => {
|
||||
// define some values for our test
|
||||
const inserterX = 14.5; // rounds to 15
|
||||
const inserterY = -17.5; // rounds to -17, +64 would be 47
|
||||
const inserterName = "inserter";
|
||||
|
||||
let store // make our store elevated
|
||||
it("is a constructor", ()=>{
|
||||
assert.equal(typeof chunkStore, "function");
|
||||
// create a new store and elevate it
|
||||
store = new chunkStore("910239"/* instanceID, use defaults for the rest */, undefined, "./database/unitTests/chunkStore/");
|
||||
assert.equal(store.name, "910239");
|
||||
});
|
||||
it("allows you to store the locations of entities", done => {
|
||||
store.setEntity(inserterX, inserterY, {name:inserterName, otherData:"can be arbitrary"}).then(() => {
|
||||
done();
|
||||
// nothing to do here really
|
||||
});
|
||||
});
|
||||
it("returns entities at positions you query for", done => {
|
||||
setTimeout(()=>{
|
||||
store.getEntity(inserterX, inserterY).then(entities => {
|
||||
entity = entities[0]
|
||||
// console.log(entity)
|
||||
assert.equal(entity.x, 14);
|
||||
assert.equal(entity.y, -18);
|
||||
assert.equal(entity.entity.name, "inserter");
|
||||
assert.equal(entity.entity.otherData, "can be arbitrary");
|
||||
done();
|
||||
});
|
||||
},300);
|
||||
});
|
||||
it("deletes entities when not provided with an object as 3rd arg", done => {
|
||||
store.setEntity(inserterX, inserterY).then(entity => {
|
||||
assert(entity === undefined);
|
||||
setTimeout(()=>{
|
||||
store.getEntity(inserterX, inserterY).then(entities => {
|
||||
// console.log(entities)
|
||||
assert.equal(entities.length, 0, "Why are we getting results, it should be empty?");
|
||||
done();
|
||||
});
|
||||
},300);
|
||||
});
|
||||
});
|
||||
describe("validates input by throwing when it is invalid", ()=>{
|
||||
it("throws if called without a string as the first parameter", ()=>{
|
||||
assert.throws(()=>{
|
||||
let throwingChunkStore = new chunkStore(undefined, undefined, "asdasda");
|
||||
});
|
||||
assert.throws(()=>{
|
||||
let throwingChunkStore = new chunkStore({}, undefined, "asdasdas");
|
||||
});
|
||||
});
|
||||
it("converts numbers to strings automatically if they are provided as the first parameter", ()=>{
|
||||
assert.ok(()=>{
|
||||
let nonThrowingChunkStore = new chunkStore(42, undefined, "./database/unitTests/chunkStore/");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -13,4 +13,5 @@ module.exports = {
|
||||
description: modInfo.description,
|
||||
// We'll send everything in this file to your stdin. Beware.
|
||||
scriptOutputFileSubscription: "remoteMap.txt",
|
||||
masterPlugin: "masterPlugin.js",
|
||||
}
|
||||
@@ -1,130 +1,71 @@
|
||||
const needle = require("needle");
|
||||
const ioClient = require("socket.io-client");
|
||||
|
||||
const chunkStore = require("./chunkStore.js");
|
||||
|
||||
const moduleConfig = require("./config"); // not to be confused with clusterio config. This config is private to this plugin.
|
||||
|
||||
class remoteMap {
|
||||
constructor(slaveConfig, messageInterface){
|
||||
this.config = slaveConfig;
|
||||
const debug = true
|
||||
module.exports = class remoteMap {
|
||||
constructor(mergedConfig, messageInterface, extras){
|
||||
this.messageInterface = messageInterface;
|
||||
this.config = mergedConfig;
|
||||
this.socket = extras.socket;
|
||||
this.instances = {};
|
||||
|
||||
// initialize chunk database
|
||||
this.chunkMap = new chunkStore(this.config.unique, 64, "./database/chunkStore/");
|
||||
this.chunkMap.onEntityChange(entity => {
|
||||
// this.messageInterface("Entity changed: ")
|
||||
// this.messageInterface(entity);
|
||||
// check valid coordinates (dunno what could go wrong, but can never be too sure)
|
||||
if(entity && entity.x !== undefined && !isNaN(Number(entity.x)) && entity.y !== undefined && !isNaN(Number(entity.y))){
|
||||
this.socket.emit("sendEntity", entity);
|
||||
this.socket.on("hello", () => this.socket.emit("registerMapServer", {
|
||||
instanceID: this.config.unique, // instanceID
|
||||
}));
|
||||
|
||||
this.socket.on("createEntity", req => {
|
||||
// req = {name:"", position:{x,y}, ?direction:4}
|
||||
// game.player.surface.create_entity{name="small-ship-wreck", position={game.player.position.x-2, game.player.position.y+1}, direction=..., force=...}
|
||||
this.messageInterface("placing "+JSON.stringify(req));
|
||||
if(req && req.name && typeof req.name == "string" && req.position && !isNaN(Number(req.position.x)) && !isNaN(Number(req.position.y))){
|
||||
// delete any entity in that spot first, just in case to avoid overlaps
|
||||
this.messageInterface('/silent-command local toDelete = game.surfaces[1].find_entities({{'+req.position.x+','+req.position.y+'},{'+(req.position.x+0.5)+','+(req.position.y+0.5)+'}}) for i, entity in pairs(toDelete) do entity.destroy() end');
|
||||
// build the entity
|
||||
let command = '/silent-command game.surfaces[1].create_entity{name="'+req.name+'", position={'+Number(req.position.x)+', '+Number(req.position.y)+'}, force="player"'
|
||||
if(req.direction) command += ', direction='+req.direction;
|
||||
command += '}';
|
||||
this.messageInterface(command); // execute command
|
||||
}
|
||||
});
|
||||
})
|
||||
this.socket.on("deleteEntity", req => {
|
||||
// req: {position: {x, y}}
|
||||
this.messageInterface('/silent-command local toDelete = game.surfaces[1].find_entities({{'+req.position.x+','+req.position.y+'},{'+(req.position.x+0.5)+','+(req.position.y+0.5)+'}}) for i, entity in pairs(toDelete) do entity.destroy() end');
|
||||
})
|
||||
this.socket.on("getChunk", async (data, callback) => {
|
||||
let chunkData = await this.messageInterface(`/silent-command remote.call("remoteMap", "exportChunk", ${data.x}, ${data.y})`)
|
||||
console.log(`Sending chunk data: ${chunkData}`)
|
||||
callback(chunkData);
|
||||
})
|
||||
|
||||
// set up websocket communication and handle requests from web interface users (which are going through master)
|
||||
// socket should be a global
|
||||
this.socket = ioClient("http://"+this.config.masterIP+":"+this.config.masterPort);
|
||||
this.socket.on("hello", data => {
|
||||
this.socket.emit("registerSlaveMapper", {instanceID: this.config.unique});
|
||||
setInterval(()=>{
|
||||
this.socket.emit("heartbeat"); // send our heartbeat to prevent being assumed dead
|
||||
},10000);
|
||||
});
|
||||
this.socket.on("getChunk", req => {
|
||||
this.messageInterface("getChunk is not supported!");
|
||||
/*this.chunkMap.getChunk(req.x, req.y).then(chunk => {
|
||||
chunk.requesterID = req.requesterID;
|
||||
this.socket.emit("sendChunk", chunk);
|
||||
});*/
|
||||
});
|
||||
this.socket.on("getEntity", req => {
|
||||
// check for valid coordinates
|
||||
if(req.x !== undefined && !isNaN(Number(req.x)) && req.y !== undefined && !isNaN(Number(req.y))){
|
||||
this.chunkMap.getEntity(req.x, req.y).then(entities => {
|
||||
if(entities && entities.length > 0){
|
||||
entities.forEach(entity => {
|
||||
this.socket.emit("sendEntity", entity);
|
||||
});
|
||||
// detect new entities created/deleted on the server and update the master
|
||||
setInterval(async ()=>{
|
||||
let data = await this.messageInterface(`/silent-command remote.call("remoteMap", "exportTiles")`)
|
||||
if(data.length > 1) this.socket.emit("updateEntities", data)
|
||||
}, 2000)
|
||||
}
|
||||
getInstanceName(instanceID){
|
||||
return new Promise((resolve, reject) => {
|
||||
let instance = this.instances[instanceID];
|
||||
if(!instance){
|
||||
needle.get(this.config.masterIP+":"+this.config.masterPort+ '/api/slaves', (err, response) => {
|
||||
if(err || response.statusCode != 200) {
|
||||
console.log("Unable to get JSON master/api/slaves, master might be unaccessible");
|
||||
} else if (response && response.body) {
|
||||
if(Buffer.isBuffer(response.body)) {console.log(response.body.toString("utf-8")); throw new Error();}
|
||||
try {
|
||||
for (let index in response.body)
|
||||
this.instances[index] = response.body[index].instanceName;
|
||||
} catch (e){
|
||||
console.log(e);
|
||||
return null;
|
||||
}
|
||||
instance = this.instances[instanceID]
|
||||
if (!instance) instance = instanceID; //somehow the master doesn't know the instance
|
||||
resolve(instance);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.messageInterface("socket.getEntity triggered called with invalid parameters");
|
||||
resolve(instance);
|
||||
}
|
||||
});
|
||||
// both placing and deleting entities via the web interface
|
||||
this.socket.on("placeEntity", req => {
|
||||
// game.player.surface.create_entity{name="small-ship-wreck", position={game.player.position.x-2, game.player.position.y+1}, direction=..., force=...}
|
||||
this.messageInterface("placing "+JSON.stringify(req));
|
||||
if(req.name && typeof req.name == "string" && req.position && !isNaN(Number(req.position.x)) && !isNaN(Number(req.position.y))){
|
||||
if(req.name == "deleted"){ // {{'+req.position.x+','+req.position.y+'},{'+req.position.x+','+req.position.y+'}}
|
||||
// delete entity from game world using LUA command
|
||||
this.messageInterface('/silent-command local toDelete = game.surfaces[1].find_entities({{'+req.position.x+','+req.position.y+'},{'+(req.position.x+1)+','+(req.position.y+1)+'}}) for i, entity in pairs(toDelete) do entity.die() end');
|
||||
// this position is now empty, delete whatever was there from DB
|
||||
this.deleteEntity(req.position.x, req.position.y);
|
||||
} else {
|
||||
let command = '/silent-command game.surfaces[1].create_entity{name="'+req.name+'", position={'+Number(req.position.x)+', '+Number(req.position.y)+'}, force="neutral"'
|
||||
if(req.direction) command += ', direction='+req.direction;
|
||||
command += '}';
|
||||
this.messageInterface(command); // execute command
|
||||
|
||||
// add entity to the chunkStore database because create_entity does not trigger any events
|
||||
let name = req.name;
|
||||
let entity = {name};
|
||||
if(typeof req.direction == "string"){
|
||||
entity.rot = req.direction;
|
||||
}
|
||||
this.addEntity(req.position.x, req.position.y, entity);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.messageInterface("/silent-command game.print('"+moduleConfig.name+" version "+moduleConfig.version+" enabled')");
|
||||
}
|
||||
addEntity(x,y,entity){ // add entity to database
|
||||
this.chunkMap.setEntity(Number(x), Number(y), entity).then(data => {
|
||||
this.messageInterface("Added "+entity.name+" to data "+Number(x)+", "+Number(y));
|
||||
}).catch((err)=>this.messageInterface(err));
|
||||
}
|
||||
deleteEntity(x,y){ // remove entity from database
|
||||
// this position is now empty, delete whatever was there from DB
|
||||
this.chunkMap.setEntity(x, y, "delete this entity").then(() => {
|
||||
this.messageInterface("Deleted entity at "+x+", "+y);
|
||||
}).catch((err)=>this.messageInterface(err));
|
||||
}
|
||||
scriptOutput(data){
|
||||
if(data){
|
||||
this.messageInterface(data);
|
||||
// express-transport-belt,-35.5,-14.5,[...]
|
||||
data = data.split(",");
|
||||
if(data && data[0] == undefined){
|
||||
this.messageInterface("empty: ");
|
||||
this.messageInterface(data);
|
||||
return
|
||||
}
|
||||
let name = data[0];
|
||||
let xPos = data[1];
|
||||
let yPos = data[2];
|
||||
if(xPos && yPos && name){
|
||||
if(name == "deleted"){
|
||||
// this position is now empty, delete whatever was there from DB
|
||||
this.deleteEntity(xPos, yPos);
|
||||
} else {
|
||||
let entity = {name};
|
||||
for(let i = 3; i < data.length; i++){
|
||||
if(data[i].includes("=")){
|
||||
let kv = data[i].split("=");
|
||||
entity[kv[0]] = kv[1];
|
||||
}
|
||||
}
|
||||
// save this entity in the database
|
||||
this.addEntity(xPos, yPos, entity);
|
||||
/* this.chunkMap.setEntity(xPos, yPos, entity).then(data => {
|
||||
this.messageInterface("Added "+name+" to data "+xPos+", "+yPos);
|
||||
}).catch((err)=>this.messageInterface(err)); */
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
module.exports = remoteMap;
|
||||
}
|
||||
File renamed without changes.
@@ -2,7 +2,7 @@
|
||||
{
|
||||
"name": "remoteMap",
|
||||
"title": "Clusterio remote map",
|
||||
"version": "2.2.0",
|
||||
"version": "3.0.0",
|
||||
"date": "18-11-2017",
|
||||
"author": "Danielv123",
|
||||
"dependencies": ["base >= 0.15"],
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
const fs = require("fs-extra");
|
||||
const path = require("path");
|
||||
const sanitizer = require('sanitizer');
|
||||
|
||||
const editorSockets = [];
|
||||
|
||||
const blacklistedEntities = [
|
||||
"fish",
|
||||
"player",
|
||||
]
|
||||
|
||||
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("registerMapServer", data => {
|
||||
console.log("Registered mapServer socket")
|
||||
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 => {
|
||||
console.log(`Creating entity: ${JSON.stringify(data)}`)
|
||||
this.slaves[Object.keys(this.slaves)[0]].emit("createEntity", data.entity)
|
||||
})
|
||||
socket.on("deleteEntity", data => {
|
||||
console.log(`Deleting entity: ${JSON.stringify(data)}`)
|
||||
this.slaves[Object.keys(this.slaves)[0]].emit("deleteEntity", data.entity)
|
||||
})
|
||||
socket.on("getChunk", (data, callback) => {
|
||||
console.log(`Getting chunk x:${data.x} y:${data.y} from slave`)
|
||||
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") &&
|
||||
!blacklistedEntities.includes(str) &&
|
||||
!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)
|
||||
.filter(entity => !blacklistedEntities.includes(entity.name))
|
||||
/*.map(entity => {
|
||||
if(entity.name.includes("tree")) entity.name = "tree"
|
||||
return entity
|
||||
})*/
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loaded 100 of 3789 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user