mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9a3fb3fce | ||
|
|
2b21bb36b6 | ||
|
|
e1771ecb38 | ||
|
|
70f3ad5ebd | ||
|
|
28b00ba5f6 | ||
|
|
9d885d5f91 | ||
|
|
d810b5646d | ||
|
|
10414eb4a6 | ||
|
|
3647ac7fc8 | ||
|
|
e75d9e967e | ||
|
|
25eb5e5d76 | ||
|
|
3c5b108e4b | ||
|
|
999c1b6c7a | ||
|
|
61b1b1a106 | ||
|
|
8b4ec58459 | ||
|
|
6e26d32fed | ||
|
|
74d9ec38dc | ||
|
|
dbef2bd2b3 | ||
|
|
e0b521d2fe | ||
|
|
9a032aed2e | ||
|
|
72b56a00c7 | ||
|
|
c25bf23286 | ||
|
|
9e983aff84 | ||
|
|
8973dcc6dd | ||
|
|
475128c8c4 | ||
|
|
0cdb5d0ce4 | ||
|
|
075d54f708 | ||
|
|
62f3a99feb | ||
|
|
3b1ed1a667 | ||
|
|
6a6274e85c | ||
|
|
a9cc743106 | ||
|
|
2a7758f60d | ||
|
|
7e2ca58a59 | ||
|
|
718d4ed659 | ||
|
|
6b090cddbe | ||
|
|
56c023f643 | ||
|
|
78de7236c5 | ||
|
|
97c7a69c7a | ||
|
|
0c45e57732 | ||
|
|
88eff17dfd | ||
|
|
e1374d501e | ||
|
|
3f2bd2653e | ||
|
|
5bbd879f34 | ||
|
|
81aa2141b9 | ||
|
|
9c108dd4dd | ||
|
|
6415784e8a | ||
|
|
206f3db042 | ||
|
|
fb28693dec | ||
|
|
79ff52aa20 | ||
|
|
cb4616f47c | ||
|
|
bc7f3804c8 | ||
|
|
a452749caa | ||
|
|
1e8fb85122 | ||
|
|
66c0238a3d | ||
|
|
ca0281ae58 | ||
|
|
200c8dbdeb | ||
|
|
d0afcbf0ac | ||
|
|
13a069a798 | ||
|
|
0e849488e2 | ||
|
|
6264bd9888 | ||
|
|
cbd1939a74 | ||
|
|
7cf9319624 | ||
|
|
de2141ec50 | ||
|
|
0df1b8e1fd | ||
|
|
ec0243d7cf | ||
|
|
f32624586b | ||
|
|
6ea47cb6af | ||
|
|
a204163a6c | ||
|
|
f76c84c3f1 | ||
|
|
6caef9bda9 | ||
|
|
30cda4de3b | ||
|
|
a13d4c6805 | ||
|
|
56b805e19b | ||
|
|
5fd3a6b188 | ||
|
|
9d1e60aa03 | ||
|
|
d819644b01 | ||
|
|
042230dc2c | ||
|
|
a00183e94c | ||
|
|
c95d9f9464 | ||
|
|
a1237d8cfd | ||
|
|
35101a7e32 | ||
|
|
95d06293f5 | ||
|
|
2714b09e5d | ||
|
|
e733cfde03 | ||
|
|
008ea139c6 | ||
|
|
3338c439dd | ||
|
|
b956438d8d | ||
|
|
cdbbdd41c7 | ||
|
|
e2775adcc3 | ||
|
|
d909b4aa47 | ||
|
|
49611a2062 | ||
|
|
ee6ab6ab72 | ||
|
|
de92ca96b9 | ||
|
|
77281d0662 | ||
|
|
2831c07000 | ||
|
|
6ac8c0711f | ||
|
|
c9cbc48d95 | ||
|
|
50370bf858 | ||
|
|
d1fd4c3565 | ||
|
|
8672afe940 | ||
|
|
adf6763a0d | ||
|
|
d48a824a69 | ||
|
|
f28b73cf31 | ||
|
|
fec3cf5ddf | ||
|
|
c7cfb8ce70 | ||
|
|
93c210f0c7 | ||
|
|
251ec696a3 | ||
|
|
773a2c412e | ||
|
|
c0499e7f27 | ||
|
|
0a65946bf4 | ||
|
|
233410add7 | ||
|
|
fea9fb28ca | ||
|
|
e1b510b441 | ||
|
|
34301eb381 | ||
|
|
4a0956d174 | ||
|
|
3b814eeea4 | ||
|
|
98c6795f8f | ||
|
|
21dd80df9b | ||
|
|
0b34f07954 | ||
|
|
c978e8e748 | ||
|
|
71870daaba | ||
|
|
5a60f317ba | ||
|
|
b666a909ab | ||
|
|
0afe35b67c | ||
|
|
096ead5bc4 | ||
|
|
fa4c10dce2 | ||
|
|
1f8ac50950 | ||
|
|
10d5b0da9b | ||
|
|
6c32271c56 | ||
|
|
a122f2b09e | ||
|
|
f35be2f355 | ||
|
|
d66d22a9cd | ||
|
|
7d6a5bb6d2 | ||
|
|
bf2e78d530 | ||
|
|
bf4b7fe1b8 | ||
|
|
66ba706c56 | ||
|
|
79c9ac839e | ||
|
|
7f59b200b1 | ||
|
|
d719a73328 | ||
|
|
616bb9cc8f | ||
|
|
3ae55cbd27 | ||
|
|
7ae9ff6649 | ||
|
|
0b3007d22d | ||
|
|
bd7a116860 | ||
|
|
0c9ed582cc | ||
|
|
d7bfc2193e | ||
|
|
ce9e1d876a | ||
|
|
86d4f3dba4 | ||
|
|
0fe5633926 | ||
|
|
595b4e5ac9 | ||
|
|
7d2ac5808f | ||
|
|
fddafaec09 | ||
|
|
abf4b5d926 | ||
|
|
00dfae8bd1 | ||
|
|
2ef6c93b05 | ||
|
|
2165acb171 | ||
|
|
8357fa23ce | ||
|
|
94d434c8ef | ||
|
|
56cf131c70 | ||
|
|
a2be560b8b | ||
|
|
a3e33f6164 | ||
|
|
01e96a4dcb | ||
|
|
d189a82494 | ||
|
|
c15709ddc1 | ||
|
|
3f29a722bb | ||
|
|
103409b0cd | ||
|
|
7e0b131557 | ||
|
|
f9a9722215 | ||
|
|
78061df0c2 | ||
|
|
0024b84745 | ||
|
|
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 | ||
|
|
bf19001055 | ||
|
|
443e5dc286 |
No files matched your search
+16
-3
@@ -11,8 +11,9 @@ pids
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
# Coverage directories used by tools like nyc and codecov
|
||||
coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
@@ -22,9 +23,9 @@ coverage
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
dist/
|
||||
|
||||
node_modules/*
|
||||
!node_modules/_app
|
||||
|
||||
Factorio*
|
||||
/factorio
|
||||
@@ -50,5 +51,17 @@ sharedPlugins/*
|
||||
!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/
|
||||
|
||||
\.cache-require-paths\.json
|
||||
# File used for marking plugins as disabled
|
||||
sharedPlugins/*/DISABLED
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "node"
|
||||
- 10
|
||||
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.
@@ -0,0 +1,60 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
Version 1.2.3
|
||||
-------------
|
||||
|
||||
- Disabled uploading mods to the master server by default as this is mostly
|
||||
just a waste of bandwidth with the mod portal being integrated into the
|
||||
game now.
|
||||
- Fixed researchSync breaking with heavily modded games due to the tech tree
|
||||
exceeding the default 100kb limit on JSON payloads.
|
||||
- Fixed the command inventoryImport sends referencing player instead of
|
||||
character, and trying to count the non-existant quick bar slots.
|
||||
- Fixed inventoryImport never receiving the script output after the default
|
||||
mode for reading script output changed to tail mode.
|
||||
- Fixed desync caused by the mods and loaded mods arrays in Hotpatch getting
|
||||
out of sync when plugins update their scenario mod code. To fix existing
|
||||
games you will need copy lib/scenarios/Hotpatch/hotpatch/mod-tools.lua
|
||||
over the existing mod-tools.lua in the save's hotpatch folder.
|
||||
- Updated the Windows install instructions.
|
||||
|
||||
|
||||
Version 1.2.2
|
||||
-------------
|
||||
|
||||
- Fixed possible crash with modded technologies named the same as a built-in
|
||||
Object prototype property in researchSync.
|
||||
- Fixed progress of a current infinite tech carrying over to the next one
|
||||
when researching it and another node completes it in researchSync.
|
||||
- Fixed progress of a previous infinite tech from another node being applied
|
||||
to the current one in researchSync.
|
||||
- Fixed crash in researchSync when modded technologies are present only on some
|
||||
nodes.
|
||||
- Fixed install failing due to bcrypt version less than 3 not being supported
|
||||
on node v10.
|
||||
- Reordered install instructions to avoid problem with npm creating files owned
|
||||
by root in the home directory.
|
||||
- Swapped curl out with wget in the install instructions as the latter comes
|
||||
pre-installed on Ubuntu.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Node.js versions below 10 are no longer supported.
|
||||
|
||||
|
||||
Version 1.2.1
|
||||
-------------
|
||||
|
||||
- Updated node-factorio-api to v0.3.8 to fix mod downloads randomly breaking
|
||||
([#229][#229].)
|
||||
- Fixed SIGINT being sent twice to Factorio server when interrupted by CTRL+C
|
||||
on Linux ([#217][#217].)
|
||||
- Fixed package.json incorrectly reporting the license as ISC.
|
||||
|
||||
[#217]: https://github.com/clusterio/factorioClusterio/issues/217
|
||||
[#229]: https://github.com/clusterio/factorioClusterio/issues/229
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Added authentication to the socket.io server running on the master.
|
||||
@@ -0,0 +1,41 @@
|
||||
const needle = require("needle");
|
||||
const cli = require("cli");
|
||||
const config = require("./../config.json");
|
||||
|
||||
let options = cli.parse()
|
||||
cli.setUsage(`node cluster_rcon.js /c game.player.force = "player"`)
|
||||
if(process.argv.length < 3){
|
||||
cli.getUsage()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const 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);
|
||||
})();
|
||||
@@ -0,0 +1,19 @@
|
||||
const needle = require("needle")
|
||||
const config = require("./../config.json")
|
||||
const cli = require("cli")
|
||||
|
||||
let options = cli.parse({
|
||||
name: ["n", "Name of the player to delete, case sensitive", "string"],
|
||||
token: [false, "Factorio auth token", "string", config.masterAuthToken]
|
||||
})
|
||||
if(!options.name){
|
||||
cli.getUsage()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
needle.post(config.masterIP+":"+config.masterPort+"/api/playerManager/deletePlayer", {
|
||||
name: options.name,
|
||||
token: options.token
|
||||
}, (err, resp) => {
|
||||
console.log(resp.body)
|
||||
});
|
||||
+9
-7
@@ -1,19 +1,21 @@
|
||||
FROM node:latest
|
||||
RUN apt-get update && apt install git -y
|
||||
|
||||
FROM node:10
|
||||
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 --only=production
|
||||
RUN cd factorioClusterio && curl -o factorio.tar.gz -L https://www.factorio.com/get-download/latest/headless/linux64 && tar -xf factorio.tar.gz
|
||||
|
||||
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,25 @@
|
||||
<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)
|
||||
## Important notice
|
||||
|
||||
This is the stable branch of the project. The
|
||||
[master branch](http://github.com/clusterio/factorioClusterio/tree/master)
|
||||
is undergoing significant changes that breaks compatibility to plugins and
|
||||
existing installations and is not recommended for use.
|
||||
|
||||
### 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 +29,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 +49,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 +85,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,31 +95,45 @@ 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
|
||||
wget -qO - https://deb.nodesource.com/setup_10.x | sudo -E bash -
|
||||
sudo apt install -y nodejs python-dev git build-essential
|
||||
git clone -b 1.2.x https://github.com/Danielv123/factorioClusterio.git
|
||||
cd factorioClusterio
|
||||
curl -o factorio.tar.gz -L https://www.factorio.com/get-download/latest/headless/linux64
|
||||
wget -O factorio.tar.gz https://www.factorio.com/get-download/latest/headless/linux64
|
||||
tar -xf factorio.tar.gz
|
||||
npm install
|
||||
npm install --only=production
|
||||
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.
|
||||
downloads and installs nodejs, git and clusterio. To specify a version, change "latest" in the link to a version number like 0.14.21.
|
||||
|
||||
*Copy config.json.dist to config.json* - Otherwise it will crash, as you'd expect.
|
||||
Optional step (if you want to use pm2):
|
||||
|
||||
sudo npm install pm2 -g
|
||||
|
||||
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**
|
||||
|
||||
node master.js
|
||||
|
||||
Or with pm2 (it's recommened to run it without pm2 first):
|
||||
|
||||
pm2 start master.js --name master
|
||||
|
||||
|
||||
|
||||
**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
|
||||
|
||||
Add the master API key, found in the masters install directory (secret-api-token.txt) to your config.json masterAuthToken field
|
||||
|
||||
To create a new instance (its own save, set of mods and config files)
|
||||
|
||||
@@ -108,12 +147,12 @@ 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.
|
||||
@@ -128,13 +167,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 8 or 9 from http://nodejs.org
|
||||
download and install nodeJS 10 from http://nodejs.org
|
||||
|
||||
download and install git from https://git-scm.com/
|
||||
|
||||
@@ -142,33 +177,57 @@ reboot when you are done, then proceed to the next steps. *reboots matter*
|
||||
|
||||
**Master**
|
||||
|
||||
1. download and run https://puu.sh/toFHl/01eebbb333.bat
|
||||
1. Open PowerShell or Command prompt in the directory you want to install to and run the following commands.
|
||||
|
||||
2. Copy config.json.dist to config.json
|
||||
git clone -b 1.2.x https://github.com/clusterio/factorioClusterio
|
||||
cd factorioClusterio
|
||||
npm install --only=production
|
||||
copy config.json.dist config.json
|
||||
|
||||
3. Follow the instructions in the bat file
|
||||
2. Obtain Factorio by either of these two methods:
|
||||
|
||||
4. Run `node client.js manage shared mods add clusterio`
|
||||
- Via the stand alone version on from their website
|
||||
|
||||
5. type `node master.js` to start the server
|
||||
1. Download the MS Windows (64-bit zip package) from https://www.factorio.com/download
|
||||
|
||||
2. Open the zip file and drag the folder called "Factorio_0.17.x" into the factorioClusterio folder
|
||||
|
||||
3. Rename the folder to "factorio"
|
||||
|
||||
- Via steam installation
|
||||
|
||||
1. Locate the game files by right clicking the game in steam, selecting properties, then Local Files, then Browse local files.
|
||||
|
||||
2. Go to the parent folder of the folder that Steam opened and copy the Factorio folder into the factorioClusterio folder
|
||||
|
||||
3. Rename the folder to "factorio"
|
||||
|
||||
|
||||
3. Open `config.json` with a text editor and configure as desired.
|
||||
|
||||
4. Run `node master.js` to generate the athentication token into secret-api-token.txt
|
||||
|
||||
5. Run `node master.js` again to start the master server.
|
||||
|
||||
**Server Host**
|
||||
|
||||
1. download and run https://puu.sh/toFHl/01eebbb333.bat
|
||||
1. Do step 1 and 2 of the Master section above *OR* use the same folder that was created in that section.
|
||||
|
||||
2. Copy config.json.dist to config.json
|
||||
2. Open `config.json` with a text editor and configure as desired. You will need to set masterAuthToken to string found in secret-api-token.txt on the master server.
|
||||
|
||||
3. Follow the instructions given
|
||||
3. Optionally run the command `node client.js manage shared mods add clusterio` to add the clusterio mod (needed for item teleports.)
|
||||
|
||||
4. Type `node client.js start [instancename]` to create a new instance.
|
||||
4. Run `node client.js start [instancename]` to create a new instance. Repeat it again to start the 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)
|
||||
e war
|
||||
Change masterPort to something like 8080 (provided by master server owner)
|
||||
Change `masterIP `to something like `31.152.123.14` (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 `masterPort` to something like `8080` (provided by master server owner)
|
||||
|
||||
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**
|
||||
|
||||
@@ -180,6 +239,47 @@ Fancy game client that does the following steps automatically, but is really old
|
||||
|
||||
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.
|
||||
|
||||
According to [this link](https://askubuntu.com/questions/839520/open-port-443-for-a-node-web-app) if you manually installed node.js following the above instructions, you may need to run the following command to fix this issue:
|
||||
|
||||
sudo setcap 'cap_net_bind_service=+ep' $(readlink -f $(which node))
|
||||
|
||||
### 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**
|
||||
@@ -200,7 +300,7 @@ node client.js list
|
||||
```
|
||||
git pull
|
||||
|
||||
npm install
|
||||
npm install --only=production
|
||||
```
|
||||
|
||||
3. Download the latest version of the factorioClusterioMod from its github repo
|
||||
|
||||
Binary file not shown.
+18
-7
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"__comment": "Where factorio folder. Should be in the same folder as this config file.",
|
||||
"factorioDirectory": "factorio",
|
||||
"instanceDirectory": "instances",
|
||||
"factorioDirectory": "./factorio",
|
||||
"instanceDirectory": "./instances",
|
||||
"databaseDirectory": "./database",
|
||||
|
||||
"__comment1": "IP players should connect to to join your servers, should be portforwarded and stuff",
|
||||
"publicIP": "localhost",
|
||||
@@ -9,11 +10,11 @@
|
||||
"__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, set to 0 or an empty string to not listen for HTTP requests",
|
||||
"__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 use HTTPS.",
|
||||
"sslPort": 443,
|
||||
"__commentsslPort": "HTTPS port for the master to listen to. Set to 0 or an empty string to not listen for HTTPS.",
|
||||
"sslPort": 0,
|
||||
"sslCert": "database/certificates/cert.crt",
|
||||
"sslPrivKey": "database/certificates/cert.key",
|
||||
|
||||
@@ -46,7 +47,17 @@
|
||||
"entriesPerSecond": 0.01666666667
|
||||
},
|
||||
"logItemTransfers": false,
|
||||
"uploadModsToMaster": true,
|
||||
"__comment6.5": "When the master is undersupplied, we have 3 different modes to 'fairly' distribute items. First to ask gets all, dole division using weird formula or experimental neural net tries to make stuff as fair as possible. Feel free to play around.",
|
||||
"disableFairItemDistribution": false,
|
||||
"useNeuralNetDoleDivider": true,
|
||||
"uploadModsToMaster": false,
|
||||
"msBetweenCommands": 10,
|
||||
"allowRemoteCommandExecution": true
|
||||
"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
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
toc:
|
||||
- name: Master Server API
|
||||
- clusterioMaster
|
||||
- name: Type declarations
|
||||
- itemStack
|
||||
- name: Internal modules
|
||||
description: |
|
||||
Here are some helper functions for navigation.
|
||||
- modManager
|
||||
- getFactorioLocale
|
||||
@@ -0,0 +1,57 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const NodeInstance_1 = require("./NodeInstance");
|
||||
const Point_1 = require("./Point");
|
||||
const Grid_1 = require("./Grid");
|
||||
/**
|
||||
* Manages all nodes within the cluster
|
||||
*/
|
||||
class ClusterManager {
|
||||
constructor() {
|
||||
this.Grid = new Grid_1.Grid();
|
||||
}
|
||||
/**
|
||||
* Generates a x*y grid of 1x1 nodes
|
||||
* @param width How wide to make the node grid
|
||||
* @param height How high to make the node grid
|
||||
* @param nodeNamePrefix The prefix used for the initial node names. The name will be in the format <prefix>_x_y.
|
||||
*/
|
||||
GenerateBasicGridOfNodes(width, height, nodeNamePrefix) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
for (let y = 0; y < height; y++) {
|
||||
let newNode = new NodeInstance_1.NodeInstance(nodeNamePrefix + "-" + (x + 1) + "-" + (y + 1), new Point_1.Point(x, y), 1, 1);
|
||||
this.Grid.AddNodeToGrid(newNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Assigns all nodes that currently do not have a server assigned to the given server
|
||||
* @param server Server to assign all unassigned nodes to
|
||||
*/
|
||||
AssignAllUnassignedNodesToServer(server) {
|
||||
this.CheckServerArray(server);
|
||||
this.Grid.GetNodes()
|
||||
.filter(node => { node.Server === null; })
|
||||
.forEach(node => {
|
||||
server.AddNodeInstance(node);
|
||||
node.Server = server;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Assigns a node to a server
|
||||
* @param x x co-ordinate of the node
|
||||
* @param y y co-ordinate of the node
|
||||
* @param serverName server name
|
||||
*/
|
||||
AssignNodeToServer(x, y, server) {
|
||||
this._nodeArray[x][y].Server = server;
|
||||
}
|
||||
CheckServerArray(server) {
|
||||
let s = this._servers[server.Name];
|
||||
if (s == null) {
|
||||
this._servers[server.Name] = server;
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.ClusterManager = ClusterManager;
|
||||
//# sourceMappingURL=ClusterManager.js.map
|
||||
@@ -0,0 +1,112 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const child_process = require("child_process");
|
||||
const needle = require("needle");
|
||||
const Helpers_1 = require("./Helpers");
|
||||
const fs = require("fs");
|
||||
const util = require("util");
|
||||
/**
|
||||
* Contains all interactions between the cluster model, the
|
||||
* Clusterio master.js/client.js infrastructure and the operating system.
|
||||
*/
|
||||
class ClusterioMasterProxy {
|
||||
constructor(masterIP, masterPort, masterAuthToken, serviceHostType) {
|
||||
this._masterIP = `${masterIP}:${masterPort}`;
|
||||
this._needleOptionsWithTokenAuthHeader = {
|
||||
headers: {
|
||||
'x-access-token': masterAuthToken,
|
||||
},
|
||||
};
|
||||
}
|
||||
async RunRconCommand(instanceId, command, logDescription) {
|
||||
console.log("Running command: " + logDescription);
|
||||
let request = { instanceID: instanceId, command };
|
||||
let response = await needle("post", this._masterIP + "/api/runCommand", request, this._needleOptionsWithTokenAuthHeader);
|
||||
}
|
||||
async GetSlaves() {
|
||||
let response = await needle("get", this._masterIP + "/api/slaves", this._needleOptionsWithTokenAuthHeader);
|
||||
let body = response.body;
|
||||
let slaves = new Array();
|
||||
Object.entries(body).forEach((keyValuePair, index, array) => {
|
||||
let slave = keyValuePair[1];
|
||||
slave.id = keyValuePair[0];
|
||||
slaves.push(slave);
|
||||
});
|
||||
return slaves;
|
||||
}
|
||||
async CreateNodeInstancesOnLocalServer(nodes) {
|
||||
await Helpers_1.asyncForEach(nodes, async (node) => {
|
||||
await this.CreateNodeInstanceOnLocalServer(node);
|
||||
});
|
||||
await Helpers_1.asyncForEach(nodes, async (node) => {
|
||||
await this.CreateTeleportZonesForANodeInstance(node);
|
||||
});
|
||||
await this.sleep(1000);
|
||||
await Helpers_1.asyncForEach(nodes, async (node) => {
|
||||
await this.CreateTeleportRestrictionsForANodeInstance(node);
|
||||
await this.sleep(100);
|
||||
});
|
||||
}
|
||||
async CreateNodeInstanceOnLocalServer(node) {
|
||||
console.log(`Creating instance ${node.Name}...`);
|
||||
await this.CreateNodeInstanceOnLocalServerUsingPm2(node);
|
||||
console.log("Waiting for node to connect to cluster master...");
|
||||
while (true) {
|
||||
await this.sleep(1000);
|
||||
//TODO: Use /api/getSlaveMeta/
|
||||
let slaves = await this.GetSlaves();
|
||||
let slave = slaves.find((slave, index, array) => slave.instanceName === node.Name);
|
||||
if (slave != null) {
|
||||
node.ClusterioWorldId = slave.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
async CreateNodeInstanceOnLocalServerUsingPm2(node) {
|
||||
let proc = child_process.spawn("pm2", ['start', '--name', node.Name, 'client.js', '--', 'start', node.Name]);
|
||||
proc.stdout.on('data', (data) => { console.log(`stdout: ${data}`); });
|
||||
proc.stderr.on('data', data => { console.log(`stderr: ${data}`); });
|
||||
proc.on('error', error => { console.log(`error: ${error}`); });
|
||||
}
|
||||
async CreateNodeInstanceOnLocalServerUsingScreen(node) {
|
||||
// first call of client.js start for a new node creates map from HotPatch scenario then exits.
|
||||
let proc = child_process.spawn("screen", ['-dmS', node.Name, '-L', '-Logfile', node.Name + '.log', 'node', 'client.js', 'start', node.Name]);
|
||||
proc.stdout.on('data', (data) => { console.log(`stdout: ${data}`); });
|
||||
proc.stderr.on('data', data => { console.log(`stderr: ${data}`); });
|
||||
proc.on('error', error => { console.log(`error: ${error}`); });
|
||||
console.log("Waiting for creation of new map...");
|
||||
// create awaiter for fs.readFile
|
||||
const readFile = util.promisify(fs.readFile);
|
||||
while (true) {
|
||||
await this.sleep(1000);
|
||||
let fileData = await readFile(node.Name + '.log');
|
||||
if (fileData.includes('Instance created')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// start node
|
||||
proc = child_process.spawn("screen", ['-dmS', node.Name, '-L', '-Logfile', node.Name + '.log', 'node', 'client.js', 'start', node.Name]);
|
||||
proc.stdout.on('data', data => { console.log(`stdout: ${data}`); });
|
||||
proc.stderr.on('data', data => { console.log(`stderr: ${data}`); });
|
||||
proc.on('error', error => { console.log(`error: ${error}`); });
|
||||
}
|
||||
async CreateTeleportZonesForANodeInstance(nodeInstance) {
|
||||
await Helpers_1.asyncForEach(nodeInstance.TeleportZones.values(), async (zone) => {
|
||||
let o = zone.GenerateTeleportZoneInFactorioCoordinates();
|
||||
let cmd = `CreateZone('${o[0]}',${o[1]},${o[2]},${o[3]},${o[4]},${o[5]},true)`;
|
||||
await this.RunRconCommand(nodeInstance.ClusterioWorldId, `/c remote.call('trainTeleports','runCode',\"${cmd}\")`, cmd);
|
||||
});
|
||||
}
|
||||
async CreateTeleportRestrictionsForANodeInstance(nodeInstance) {
|
||||
await Helpers_1.asyncForEach(nodeInstance.TeleportZones.values(), async (zone) => {
|
||||
let o = zone.GenerateZoneRestriction();
|
||||
let cmd = `CreateZoneRestriction(${o[0]},'${o[1]}','${o[2]}')`;
|
||||
await this.RunRconCommand(nodeInstance.ClusterioWorldId, `/c remote.call('trainTeleports','runCode',\"${cmd}\")`, cmd);
|
||||
});
|
||||
}
|
||||
sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
exports.ClusterioMasterProxy = ClusterioMasterProxy;
|
||||
//# sourceMappingURL=ClusterioMasterProxy.js.map
|
||||
@@ -0,0 +1,8 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
/** The multiplier used to convert from the grid's coordinate system to factorio's.
|
||||
* 1000 means a 1x1 grid node will have a factorio map size of 1000x1000. */
|
||||
exports.GridToFactorioCoordinateSystemMultiplier = 1000;
|
||||
/** The width of teleport zones when they get created. */
|
||||
exports.TeleportZoneWidth = 50;
|
||||
//# sourceMappingURL=Config.js.map
|
||||
@@ -0,0 +1,177 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const typescript_collections_1 = require("typescript-collections");
|
||||
const Point_1 = require("./Point");
|
||||
const TeleportZone_1 = require("./TeleportZone");
|
||||
const TeleportZoneDirection_1 = require("./TeleportZoneDirection");
|
||||
class Grid {
|
||||
constructor() {
|
||||
this._nodes = new typescript_collections_1.Dictionary();
|
||||
this._minX = 0;
|
||||
this._minY = 0;
|
||||
this._maxX = 0;
|
||||
this._maxY = 0;
|
||||
}
|
||||
GetNodes() {
|
||||
return this._nodes.values();
|
||||
}
|
||||
/**
|
||||
* Adds a node to the grid
|
||||
* @param topLeftCoordinate The top left x,y coordinate of the node
|
||||
* @param nodeInstance
|
||||
*/
|
||||
AddNodeToGrid(nodeInstance) {
|
||||
let topLeftCoordinate = nodeInstance.TopLeftCoordinate;
|
||||
let pointsToSave = new Array();
|
||||
// check all points that make up nodeInstance for existing nodes
|
||||
for (let x = topLeftCoordinate.X; x < topLeftCoordinate.X + nodeInstance.Width; x++) {
|
||||
for (let y = topLeftCoordinate.Y; y < topLeftCoordinate.Y + nodeInstance.Height; y++) {
|
||||
let currentPoint = new Point_1.Point(x, y);
|
||||
let existingNode = this._nodes.getValue(currentPoint);
|
||||
if (existingNode != null) {
|
||||
throw new Error("Cannot add node to grid - collision detected with node '" + existingNode.Name + "'");
|
||||
}
|
||||
pointsToSave.push(currentPoint);
|
||||
}
|
||||
}
|
||||
// no existing nodes found to assign node to these points
|
||||
for (let i = 0; i < pointsToSave.length; i++) {
|
||||
this._nodes.setValue(pointsToSave[i], nodeInstance);
|
||||
}
|
||||
//TODO: Refactor
|
||||
//#region Set up teleport zones
|
||||
// check surrounding points for nodes
|
||||
// check north points
|
||||
let y = topLeftCoordinate.Y - 1;
|
||||
for (let x = topLeftCoordinate.X; x < topLeftCoordinate.X + nodeInstance.Width; x++) {
|
||||
let currentPoint = new Point_1.Point(x, y);
|
||||
let existingNode = this._nodes.getValue(currentPoint);
|
||||
// found a node - set up teleport zone
|
||||
if (existingNode != null) {
|
||||
// Create/Update teleport node in existing node to new node
|
||||
let teleportZoneFromExistingToNew = existingNode.TeleportZones.getValue(nodeInstance.Key);
|
||||
if (teleportZoneFromExistingToNew == null) {
|
||||
teleportZoneFromExistingToNew = new TeleportZone_1.TeleportZone(existingNode, TeleportZoneDirection_1.TeleportZoneDirection.South, currentPoint, currentPoint, existingNode.TeleportZones.size(), nodeInstance);
|
||||
existingNode.TeleportZones.setValue(nodeInstance.Key, teleportZoneFromExistingToNew);
|
||||
}
|
||||
else {
|
||||
teleportZoneFromExistingToNew.BottomRightCoordinate = currentPoint;
|
||||
}
|
||||
// Create/Update teleport node in new node to existing node
|
||||
let teleportZoneFromNewToExisting = nodeInstance.TeleportZones.getValue(existingNode.Key);
|
||||
let pointInNewNode = new Point_1.Point(currentPoint.X, currentPoint.Y + 1);
|
||||
if (teleportZoneFromNewToExisting == null) {
|
||||
teleportZoneFromNewToExisting = new TeleportZone_1.TeleportZone(nodeInstance, TeleportZoneDirection_1.TeleportZoneDirection.North, pointInNewNode, pointInNewNode, nodeInstance.TeleportZones.size(), existingNode);
|
||||
nodeInstance.TeleportZones.setValue(existingNode.Key, teleportZoneFromNewToExisting);
|
||||
}
|
||||
else {
|
||||
teleportZoneFromNewToExisting.BottomRightCoordinate = currentPoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
// check south points
|
||||
y = topLeftCoordinate.Y + nodeInstance.Height + 1;
|
||||
for (let x = topLeftCoordinate.X; x < topLeftCoordinate.X + nodeInstance.Width; x++) {
|
||||
let currentPoint = new Point_1.Point(x, y);
|
||||
let existingNode = this._nodes.getValue(currentPoint);
|
||||
// found a node - set up teleport zone
|
||||
if (existingNode != null) {
|
||||
// Create/Update teleport node in existing node to new node
|
||||
let teleportZoneFromExistingToNew = existingNode.TeleportZones.getValue(nodeInstance.Key);
|
||||
if (teleportZoneFromExistingToNew == null) {
|
||||
teleportZoneFromExistingToNew = new TeleportZone_1.TeleportZone(existingNode, TeleportZoneDirection_1.TeleportZoneDirection.North, currentPoint, currentPoint, existingNode.TeleportZones.size(), nodeInstance);
|
||||
existingNode.TeleportZones.setValue(nodeInstance.Key, teleportZoneFromExistingToNew);
|
||||
}
|
||||
else {
|
||||
teleportZoneFromExistingToNew.BottomRightCoordinate = currentPoint;
|
||||
}
|
||||
// Create/Update teleport node in new node to existing node
|
||||
let teleportZoneFromNewToExisting = nodeInstance.TeleportZones.getValue(existingNode.Key);
|
||||
let pointInNewNode = new Point_1.Point(currentPoint.X, currentPoint.Y - 1);
|
||||
if (teleportZoneFromNewToExisting == null) {
|
||||
teleportZoneFromNewToExisting = new TeleportZone_1.TeleportZone(nodeInstance, TeleportZoneDirection_1.TeleportZoneDirection.South, pointInNewNode, pointInNewNode, nodeInstance.TeleportZones.size(), existingNode);
|
||||
nodeInstance.TeleportZones.setValue(existingNode.Key, teleportZoneFromNewToExisting);
|
||||
}
|
||||
else {
|
||||
teleportZoneFromNewToExisting.BottomRightCoordinate = currentPoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
// check west points
|
||||
let x = topLeftCoordinate.X - 1;
|
||||
for (let y = topLeftCoordinate.Y; y < topLeftCoordinate.Y + nodeInstance.Height; y++) {
|
||||
let currentPoint = new Point_1.Point(x, y);
|
||||
let existingNode = this._nodes.getValue(currentPoint);
|
||||
// found a node - set up teleport zone
|
||||
if (existingNode != null) {
|
||||
// Create/Update teleport node in existing node to new node
|
||||
let teleportZoneFromExistingToNew = existingNode.TeleportZones.getValue(nodeInstance.Key);
|
||||
if (teleportZoneFromExistingToNew == null) {
|
||||
teleportZoneFromExistingToNew = new TeleportZone_1.TeleportZone(existingNode, TeleportZoneDirection_1.TeleportZoneDirection.East, currentPoint, currentPoint, existingNode.TeleportZones.size(), nodeInstance);
|
||||
existingNode.TeleportZones.setValue(nodeInstance.Key, teleportZoneFromExistingToNew);
|
||||
}
|
||||
else {
|
||||
teleportZoneFromExistingToNew.BottomRightCoordinate = currentPoint;
|
||||
}
|
||||
// Create/Update teleport node in new node to existing node
|
||||
let teleportZoneFromNewToExisting = nodeInstance.TeleportZones.getValue(existingNode.Key);
|
||||
let pointInNewNode = new Point_1.Point(currentPoint.X + 1, currentPoint.Y);
|
||||
if (teleportZoneFromNewToExisting == null) {
|
||||
teleportZoneFromNewToExisting = new TeleportZone_1.TeleportZone(nodeInstance, TeleportZoneDirection_1.TeleportZoneDirection.West, pointInNewNode, pointInNewNode, nodeInstance.TeleportZones.size(), existingNode);
|
||||
nodeInstance.TeleportZones.setValue(existingNode.Key, teleportZoneFromNewToExisting);
|
||||
}
|
||||
else {
|
||||
teleportZoneFromNewToExisting.BottomRightCoordinate = currentPoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
// check east points
|
||||
x = topLeftCoordinate.X + nodeInstance.Width + 1;
|
||||
for (let y = topLeftCoordinate.Y; y < topLeftCoordinate.Y + nodeInstance.Height; y++) {
|
||||
let currentPoint = new Point_1.Point(x, y);
|
||||
let existingNode = this._nodes.getValue(currentPoint);
|
||||
// found a node - set up teleport zone
|
||||
if (existingNode != null) {
|
||||
// Create/Update teleport node in existing node to new node
|
||||
let teleportZoneFromExistingToNew = existingNode.TeleportZones.getValue(nodeInstance.Key);
|
||||
if (teleportZoneFromExistingToNew == null) {
|
||||
teleportZoneFromExistingToNew = new TeleportZone_1.TeleportZone(existingNode, TeleportZoneDirection_1.TeleportZoneDirection.West, currentPoint, currentPoint, existingNode.TeleportZones.size(), nodeInstance);
|
||||
existingNode.TeleportZones.setValue(nodeInstance.Key, teleportZoneFromExistingToNew);
|
||||
}
|
||||
else {
|
||||
teleportZoneFromExistingToNew.BottomRightCoordinate = currentPoint;
|
||||
}
|
||||
// Create/Update teleport node in new node to existing node
|
||||
let teleportZoneFromNewToExisting = nodeInstance.TeleportZones.getValue(existingNode.Key);
|
||||
let pointInNewNode = new Point_1.Point(currentPoint.X - 1, currentPoint.Y);
|
||||
if (teleportZoneFromNewToExisting == null) {
|
||||
teleportZoneFromNewToExisting = new TeleportZone_1.TeleportZone(nodeInstance, TeleportZoneDirection_1.TeleportZoneDirection.East, pointInNewNode, pointInNewNode, nodeInstance.TeleportZones.size(), existingNode);
|
||||
nodeInstance.TeleportZones.setValue(existingNode.Key, teleportZoneFromNewToExisting);
|
||||
}
|
||||
else {
|
||||
teleportZoneFromNewToExisting.BottomRightCoordinate = currentPoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
this._nodes.setValue(topLeftCoordinate, nodeInstance);
|
||||
this._minX = Math.min(this._minX, topLeftCoordinate.X);
|
||||
this._minY = Math.min(this._minY, topLeftCoordinate.Y);
|
||||
this._maxX = Math.max(this._maxX, topLeftCoordinate.X + nodeInstance.Width);
|
||||
this._maxY = Math.max(this._maxY, topLeftCoordinate.Y + nodeInstance.Height);
|
||||
}
|
||||
get Width() {
|
||||
return this._maxX - this._minX;
|
||||
}
|
||||
get Height() {
|
||||
return this._maxY - this._minY;
|
||||
}
|
||||
get TopLeftCoordinate() {
|
||||
return new Point_1.Point(this._minX, this._minY);
|
||||
}
|
||||
get BottomRightCoordinate() {
|
||||
return new Point_1.Point(this._maxX, this._maxY);
|
||||
}
|
||||
}
|
||||
exports.Grid = Grid;
|
||||
//# sourceMappingURL=Grid.js.map
|
||||
@@ -0,0 +1,10 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
//TODO: Make generic
|
||||
async function asyncForEach(array, callback) {
|
||||
for (let index = 0; index < array.length; index++) {
|
||||
await callback(array[index], index, array);
|
||||
}
|
||||
}
|
||||
exports.asyncForEach = asyncForEach;
|
||||
//# sourceMappingURL=Helpers.js.map
|
||||
@@ -0,0 +1,3 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
//# sourceMappingURL=IApiSlaves.js.map
|
||||
@@ -0,0 +1,3 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
//# sourceMappingURL=IClusterioMasterProxy.js.map
|
||||
@@ -0,0 +1,27 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
console.log("started.");
|
||||
const ClusterioMasterProxy_1 = require("./ClusterioMasterProxy");
|
||||
const ClusterManager_1 = require("./ClusterManager");
|
||||
const argv = require('minimist')(process.argv.slice(2));
|
||||
let configPath = argv['configPath'];
|
||||
if (configPath === undefined || configPath === null || configPath === "") {
|
||||
throw new Error("configPath is null.");
|
||||
}
|
||||
;
|
||||
let config = require(configPath);
|
||||
let proxy = new ClusterioMasterProxy_1.ClusterioMasterProxy(config.masterIP, config.masterPort, config.masterAuthToken);
|
||||
let manager = new ClusterManager_1.ClusterManager();
|
||||
manager.GenerateBasicGridOfNodes(2, 2, 'GridLockTest_');
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
// async function test() {
|
||||
// await asyncForEach([1,2,3], async (v, i, a) => {
|
||||
// console.log("test" + i);
|
||||
// await sleep(5000);
|
||||
// });
|
||||
// }
|
||||
// test();
|
||||
proxy.CreateNodeInstancesOnLocalServer(manager.Grid.GetNodes());
|
||||
//# sourceMappingURL=ModelController.js.map
|
||||
@@ -0,0 +1,24 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
console.log("started.");
|
||||
const ClusterioMasterProxy_1 = require("./ClusterioMasterProxy");
|
||||
const ClusterManager_1 = require("./ClusterManager");
|
||||
const argv = require('minimist')(process.argv.slice(2));
|
||||
let configPath = argv['configPath'];
|
||||
if (configPath === undefined || configPath === null || configPath === "") {
|
||||
throw new Error("configPath is null.");
|
||||
}
|
||||
;
|
||||
let config = require(configPath);
|
||||
let proxy = new ClusterioMasterProxy_1.ClusterioMasterProxy(config.masterIP, config.masterPort, config.masterAuthToken);
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
// main
|
||||
(async () => {
|
||||
let manager = new ClusterManager_1.ClusterManager();
|
||||
manager.GenerateBasicGridOfNodes(2, 2, 'GridLockTest');
|
||||
await proxy.CreateNodeInstancesOnLocalServer(manager.Grid.GetNodes());
|
||||
return;
|
||||
})();
|
||||
//# sourceMappingURL=ModelDemo.js.map
|
||||
@@ -0,0 +1,61 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
console.log("Started.");
|
||||
const ClusterioMasterProxy_1 = require("./ClusterioMasterProxy");
|
||||
const ClusterManager_1 = require("./ClusterManager");
|
||||
const argv = require('minimist')(process.argv.slice(2));
|
||||
// arg: configPath
|
||||
// description: should point to the clusterio config.json file.
|
||||
let configPath = argv['configPath'];
|
||||
if (configPath === undefined || configPath === null || configPath === "") {
|
||||
configPath = "../../config.json";
|
||||
}
|
||||
;
|
||||
let config = require(configPath);
|
||||
// arg: mode
|
||||
// description: should be GenerateBasicGridOfNodes for now. Can extend in future.
|
||||
let mode = argv['mode'];
|
||||
if (mode === undefined || mode === null || mode === "") {
|
||||
throw new Error("mode is null. mode must be 'GenerateBasicGridOfNodes' (for now).");
|
||||
}
|
||||
;
|
||||
// arg: serviceHostType
|
||||
// description: what program to use to host the client.js instances.
|
||||
let serviceHostType = argv['serviceHostType'];
|
||||
if (serviceHostType === undefined || serviceHostType === null || serviceHostType === "") {
|
||||
throw new Error("serviceHostType is null. serviceHostType must be either 'pm2' or 'screen'.");
|
||||
}
|
||||
;
|
||||
if (serviceHostType != "pm2" && serviceHostType != "screen") {
|
||||
throw new Error("serviceHostType must be either 'pm2' or 'screen'.");
|
||||
}
|
||||
// arg: width
|
||||
// description: width of node grid
|
||||
let width = argv['width'];
|
||||
if (width === undefined || width === null || width === "") {
|
||||
throw new Error("width is null. width must be an int > 0.");
|
||||
}
|
||||
;
|
||||
// arg: height
|
||||
// description: height of node grid
|
||||
let height = argv['height'];
|
||||
if (height === undefined || height === null || height === "") {
|
||||
throw new Error("height is null. height must be an int > 0.");
|
||||
}
|
||||
;
|
||||
// arg: namePrefix
|
||||
// description: namePrefix of node grid
|
||||
let namePrefix = argv['namePrefix'];
|
||||
if (namePrefix === undefined || namePrefix === null || namePrefix === "") {
|
||||
throw new Error("namePrefix is null.");
|
||||
}
|
||||
;
|
||||
let proxy = new ClusterioMasterProxy_1.ClusterioMasterProxy(config.masterIP, config.masterPort, config.masterAuthToken, serviceHostType);
|
||||
// main
|
||||
(async () => {
|
||||
let manager = new ClusterManager_1.ClusterManager();
|
||||
manager.GenerateBasicGridOfNodes(width, height, namePrefix);
|
||||
await proxy.CreateNodeInstancesOnLocalServer(manager.Grid.GetNodes());
|
||||
return;
|
||||
})();
|
||||
//# sourceMappingURL=ModelRunner.js.map
|
||||
@@ -0,0 +1,29 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const Point_1 = require("./Point");
|
||||
const typescript_collections_1 = require("typescript-collections");
|
||||
/**
|
||||
* An instance of a Factorio server. Multiple nodes can run on the same server.
|
||||
*/
|
||||
class NodeInstance {
|
||||
constructor(name, topLeftCoordinate, width, height) {
|
||||
this.TeleportZones = new typescript_collections_1.Dictionary();
|
||||
this.Name = name;
|
||||
this.Width = width;
|
||||
this.Height = height;
|
||||
this.TopLeftCoordinate = topLeftCoordinate;
|
||||
}
|
||||
get BottomRightCoordinate() {
|
||||
return new Point_1.Point(this.TopLeftCoordinate.X + this.Width, this.TopLeftCoordinate.Y + this.Height);
|
||||
}
|
||||
// public AddTeleportZone(name: string, topLeftCoordinate: Point, bottomRightCoordinate: Point) {
|
||||
// this.TeleportZones.push(new TeleportZone(this, topLeftCoordinate, bottomRightCoordinate, name));
|
||||
// }
|
||||
AddTeleportZonePoint() {
|
||||
}
|
||||
get Key() {
|
||||
return this.Name;
|
||||
}
|
||||
}
|
||||
exports.NodeInstance = NodeInstance;
|
||||
//# sourceMappingURL=NodeInstance.js.map
|
||||
@@ -0,0 +1,57 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
class Point {
|
||||
/**
|
||||
* Constructs a Point. Will not accept NaNs.
|
||||
* @param x A number. Cannot be NaN.
|
||||
* @param y A number. Cannot be NaN.
|
||||
*/
|
||||
constructor(x, y) {
|
||||
this.toString = () => {
|
||||
return "(" + this.X + "," + this.Y + ")";
|
||||
};
|
||||
if (Number.isNaN(x)) {
|
||||
throw new Error("x is NaN. Will not construct Point.");
|
||||
}
|
||||
if (Number.isNaN(y)) {
|
||||
throw new Error("y is NaN. Will not construct Point.");
|
||||
}
|
||||
this.X = x;
|
||||
this.Y = y;
|
||||
}
|
||||
/**
|
||||
* Constructs a Point from a string representation in the format (x,y). Will not accept NaNs.
|
||||
* @param vectorAsString A string in the format (x,y). Will not accept NaNs.
|
||||
*/
|
||||
static FromString(vectorAsString) {
|
||||
this.CheckChar('(', vectorAsString[0], vectorAsString);
|
||||
this.CheckChar(')', vectorAsString[vectorAsString.length - 1], vectorAsString);
|
||||
if (vectorAsString.indexOf(',') === -1) {
|
||||
throw new Error("vectorAsString is an invalid format. Cannot find ','. fullString: " + vectorAsString);
|
||||
}
|
||||
let splitStr = vectorAsString.substr(1, vectorAsString.length - 2).split(',');
|
||||
if (splitStr.length != 2) {
|
||||
throw new Error("vectorAsString is an invalid format. Expecting string in format (x,y). fullString: " + vectorAsString);
|
||||
}
|
||||
let x = parseInt(splitStr[0]);
|
||||
let y = parseInt(splitStr[1]);
|
||||
let v = new Point(x, y);
|
||||
return v;
|
||||
}
|
||||
static CheckChar(expectedChar, actualChar, fullString) {
|
||||
if (actualChar != expectedChar) {
|
||||
throw new Error("vectorAsString is an invalid format. Expecting: '" + expectedChar + "'. Actual: '" + actualChar + "'. fullString: " + fullString);
|
||||
}
|
||||
}
|
||||
equals(obj) {
|
||||
return this.X === obj.X && this.Y === obj.Y;
|
||||
}
|
||||
Add(p) {
|
||||
return new Point(this.X + p.X, this.Y + p.Y);
|
||||
}
|
||||
Subtract(p) {
|
||||
return new Point(this.X - p.X, this.Y - p.Y);
|
||||
}
|
||||
}
|
||||
exports.Point = Point;
|
||||
//# sourceMappingURL=Point.js.map
|
||||
@@ -0,0 +1,16 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
/**
|
||||
* A server. Can contain multiple nodes (Factorio server instances).
|
||||
*/
|
||||
class Server {
|
||||
constructor(name) {
|
||||
this.Nodes = new Array();
|
||||
this.Name = name;
|
||||
}
|
||||
AddNodeInstance(node) {
|
||||
this.Nodes.push(node);
|
||||
}
|
||||
}
|
||||
exports.Server = Server;
|
||||
//# sourceMappingURL=Server.js.map
|
||||
@@ -0,0 +1,8 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var ServiceHostType;
|
||||
(function (ServiceHostType) {
|
||||
ServiceHostType[ServiceHostType["pm2"] = 0] = "pm2";
|
||||
ServiceHostType[ServiceHostType["screen"] = 1] = "screen";
|
||||
})(ServiceHostType = exports.ServiceHostType || (exports.ServiceHostType = {}));
|
||||
//# sourceMappingURL=ServiceHostType.js.map
|
||||
@@ -0,0 +1,64 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const Point_1 = require("./Point");
|
||||
const TeleportZoneDirection_1 = require("./TeleportZoneDirection");
|
||||
const Config = require("./Config");
|
||||
class TeleportZone {
|
||||
constructor(parentNode, direction, topLeftCoordinate, bottomRightCoordinate, index, targetNode) {
|
||||
this.TopLeftCoordinate = topLeftCoordinate;
|
||||
this.BottomRightCoordinate = bottomRightCoordinate;
|
||||
this.ParentNode = parentNode;
|
||||
this.Direction = direction;
|
||||
this.Index = index;
|
||||
this.TargetNode = targetNode;
|
||||
}
|
||||
get Name() {
|
||||
return TeleportZoneDirection_1.TeleportZoneDirection[this.Direction] + " to " + this.TargetNode.Name;
|
||||
}
|
||||
// public AddRestriction(restrictingTeleportZone: TeleportZone) {
|
||||
// this.Restrictions.push(new TeleportZoneRestriction(this, restrictingTeleportZone));
|
||||
// }
|
||||
/**
|
||||
* Converts this teleport zone into Factorio map co-ordinates
|
||||
* @returns [name, topleft.x, topleft.y, width, height, zoneIndex]
|
||||
*/
|
||||
GenerateTeleportZoneInFactorioCoordinates() {
|
||||
// get where this zone is relative to the node itself
|
||||
let topLeftRelativeCoordinate = this.TopLeftCoordinate.Subtract(this.ParentNode.TopLeftCoordinate);
|
||||
// a grid coordinate point is equivalent to a 1x1 node, so bottomright = topleft.
|
||||
// add (1,1) to translate from area representation to coordinates representing actual points.
|
||||
let bottomRightRelativeCoordinate = this.BottomRightCoordinate
|
||||
.Subtract(this.ParentNode.TopLeftCoordinate)
|
||||
.Add(new Point_1.Point(1, 1));
|
||||
// Factorio map is e.g. (-500,-500) to (500, 500) for a 1000x1000 node.
|
||||
// We therefore need to offset our co-ordinates so that (0,0) is the center and not (width/2, height/2)
|
||||
let m = Config.GridToFactorioCoordinateSystemMultiplier;
|
||||
let factorioTeleportZoneTopLeft = new Point_1.Point(topLeftRelativeCoordinate.X * m - m / 2, topLeftRelativeCoordinate.Y * m - m / 2);
|
||||
let factorioTeleportZoneBottomRight = new Point_1.Point(bottomRightRelativeCoordinate.X * m - m / 2, bottomRightRelativeCoordinate.Y * m - m / 2);
|
||||
let zoneWidth = factorioTeleportZoneBottomRight.X - factorioTeleportZoneTopLeft.X;
|
||||
let zoneHeight = factorioTeleportZoneBottomRight.Y - factorioTeleportZoneTopLeft.Y;
|
||||
let tl = factorioTeleportZoneTopLeft;
|
||||
let br = factorioTeleportZoneBottomRight;
|
||||
let zw = Config.TeleportZoneWidth;
|
||||
switch (this.Direction) {
|
||||
case TeleportZoneDirection_1.TeleportZoneDirection.North:
|
||||
return [this.Name, tl.X, tl.Y, zoneWidth, zw, this.Index + 1];
|
||||
case TeleportZoneDirection_1.TeleportZoneDirection.West:
|
||||
return [this.Name, tl.X, tl.Y, zw, zoneHeight, this.Index + 1];
|
||||
case TeleportZoneDirection_1.TeleportZoneDirection.South:
|
||||
return [this.Name, tl.X, br.Y - zw, zoneWidth, zw, this.Index + 1];
|
||||
case TeleportZoneDirection_1.TeleportZoneDirection.East:
|
||||
return [this.Name, br.X - zw, tl.Y, zw, zoneHeight, this.Index + 1];
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Generates a zone restriction from this teleport zone to target zone
|
||||
* @returns [zoneIndex, targetNodeName, targetZoneName]
|
||||
*/
|
||||
GenerateZoneRestriction() {
|
||||
let targetZone = this.TargetNode.TeleportZones.getValue(this.ParentNode.Key);
|
||||
return [this.Index + 1, this.TargetNode.Name, targetZone.Name];
|
||||
}
|
||||
}
|
||||
exports.TeleportZone = TeleportZone;
|
||||
//# sourceMappingURL=TeleportZone.js.map
|
||||
@@ -0,0 +1,10 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var TeleportZoneDirection;
|
||||
(function (TeleportZoneDirection) {
|
||||
TeleportZoneDirection[TeleportZoneDirection["North"] = 0] = "North";
|
||||
TeleportZoneDirection[TeleportZoneDirection["East"] = 1] = "East";
|
||||
TeleportZoneDirection[TeleportZoneDirection["South"] = 2] = "South";
|
||||
TeleportZoneDirection[TeleportZoneDirection["West"] = 3] = "West";
|
||||
})(TeleportZoneDirection = exports.TeleportZoneDirection || (exports.TeleportZoneDirection = {}));
|
||||
//# sourceMappingURL=TeleportZoneDirection.js.map
|
||||
@@ -0,0 +1,10 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
class TeleportZoneRestriction {
|
||||
constructor(parentTeleportZone, restrictingTeleportZone) {
|
||||
this.ParentTeleportZone = parentTeleportZone;
|
||||
this.RestrictingTeleportZone = restrictingTeleportZone;
|
||||
}
|
||||
}
|
||||
exports.TeleportZoneRestriction = TeleportZoneRestriction;
|
||||
//# sourceMappingURL=TeleportZoneRestriction.js.map
|
||||
@@ -0,0 +1,13 @@
|
||||
"use strict";
|
||||
function __export(m) {
|
||||
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
|
||||
}
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
__export(require("./ClusterManager"));
|
||||
__export(require("./NodeInstance"));
|
||||
__export(require("./Server"));
|
||||
__export(require("./TeleportZone"));
|
||||
__export(require("./TeleportZoneRestriction"));
|
||||
__export(require("./Point"));
|
||||
__export(require("./Grid"));
|
||||
//# sourceMappingURL=index.js.map
|
||||
+25
-12
@@ -1,15 +1,28 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const config = require("./../config");
|
||||
// const config = require("./../config");
|
||||
|
||||
var 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();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {middleware};
|
||||
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,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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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 lib/clusterTools needs to be called with {config} to work");
|
||||
console.log(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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"),
|
||||
})
|
||||
@@ -0,0 +1,119 @@
|
||||
//Sigmoids...
|
||||
|
||||
function sigmoid0(x)
|
||||
{
|
||||
return x/(1+Math.abs(x));
|
||||
}
|
||||
function sigmoid1(x)
|
||||
{
|
||||
if (x>=0.0) {
|
||||
return 1.0
|
||||
} else {
|
||||
return 0.0
|
||||
}
|
||||
}
|
||||
function sigmoid2(x)
|
||||
{
|
||||
if (x>=0.0){
|
||||
return x
|
||||
} else {
|
||||
return 0.0
|
||||
}
|
||||
}
|
||||
function sigmoid3(x)
|
||||
{
|
||||
return x;
|
||||
}
|
||||
|
||||
//DOSE - Doses items for item X slave Y
|
||||
//Numreq - number of items that are requested for item X for slave Y
|
||||
//Instore - number of items for item X in master gauge (currently)
|
||||
//store_last_tick - Number of items for item X in master gauge at time of last tick
|
||||
//dole - Current dole for item X
|
||||
//carry - carry for item X slave Y
|
||||
//prev_req - Numreq for previous request for item X slave Y
|
||||
//* doleinfo_last - Object with 2 values for last "tick period": numreq - total number or requests for item, numslave - number of slaves requesting that item
|
||||
//debt - debt for item X slave Y
|
||||
|
||||
//Returns: Array of 3 elements
|
||||
//0.Number of items to give in that dose
|
||||
//1.New dole for item X
|
||||
//2.New carry for item X slave Y
|
||||
//3.New debt
|
||||
function Dose(numreq,instore,store_last_tick,dole,carry,prev_req,numreq_total_adj,debt)
|
||||
{
|
||||
numreq=Number(numreq);
|
||||
instore=Number(instore);
|
||||
var instore_adj=instore;
|
||||
if (instore_adj==0) instore_adj=0.1;
|
||||
|
||||
var b=Math.trunc(prev_req/instore_adj*numreq);
|
||||
var inp1=[numreq,instore_adj,store_last_tick/numreq,dole,carry];//0..4
|
||||
var inp2=[prev_req/numreq,numreq/numreq_total_adj,store_last_tick/numreq_total_adj,prev_req/instore_adj,b,numreq_total_adj,store_last_tick,debt];//5..12
|
||||
var inputs=inp1.concat(inp2);
|
||||
var internal=new Array(20).fill(0);
|
||||
var outputs=[0,0,0,0];
|
||||
//NN: Please don't touch :), code is generated
|
||||
|
||||
outputs[0]=inputs[7]*0.8058582544;
|
||||
outputs[0]=sigmoid2(outputs[0]);
|
||||
outputs[1]=sigmoid0(outputs[1]);
|
||||
outputs[2]=sigmoid0(outputs[2]);
|
||||
outputs[3]=inputs[6]*0.2294657379;
|
||||
outputs[3]=sigmoid0(outputs[3]);
|
||||
|
||||
debt=debt || 0;
|
||||
if (outputs[0]>0.99) outputs[0]=1.0;
|
||||
|
||||
var res1=Number(outputs[0])*numreq+outputs[3]+debt;
|
||||
var res_capped=Math.round(res1);
|
||||
|
||||
if (res_capped>numreq) res_capped=numreq;
|
||||
if (res_capped>instore) res_capped=instore;
|
||||
if (res_capped<0) res_capped=0;
|
||||
|
||||
res1=res1 || 0;
|
||||
res_capped=res_capped || 0;//Safeguard against wrong values
|
||||
debt=res1-res_capped;
|
||||
debt_cap=Math.round(numreq*1.5)+5;
|
||||
if (debt>debt_cap) debt=debt_cap;
|
||||
|
||||
carry=sigmoid0(outputs[2]*carry+outputs[0]) || 0;
|
||||
return [res_capped,dole+outputs[1],carry,debt];
|
||||
}
|
||||
|
||||
//TICK - Updates values for item X, should be called once for each item for each round of all slaves requesting that item
|
||||
//Instore - number of items for item X in master gauge (currently)
|
||||
//dole - Current dole for item X
|
||||
//store_last_tick - Number of items for item X in master gauge at time of last tick (not this one)
|
||||
//* doleinfo_last - Object with 2 values for last "tick period": numreq - total number or requests for item, numslave - number of slaves requesting that item
|
||||
//* doleinfo_new - Object with 2 values for new "tick period": numreq - total number or requests for item, numslave - number of slaves requesting that item
|
||||
|
||||
|
||||
//Returns: Array of 2 elements
|
||||
//0.New dole for item X
|
||||
//1.Guessed percentage average of items supplied in last 10 ticks
|
||||
function Tick(instore,dole,store_last_tick,numreq_total_adj)
|
||||
{
|
||||
var instore_adj=instore;
|
||||
if (instore_adj==0) instore_adj=0.1;
|
||||
var inputs=[instore,dole,store_last_tick/instore_adj,store_last_tick/numreq_total_adj,store_last_tick,numreq_total_adj];
|
||||
var internal=new Array(10).fill(0);
|
||||
var outputs=[0,0];
|
||||
//NN: Please don't touch :), code is generated
|
||||
|
||||
outputs[0]=inputs[3]*0.9364774823;
|
||||
outputs[0]=outputs[0]*1.0*0.4590721726;
|
||||
outputs[0]=sigmoid2(outputs[0]);
|
||||
outputs[1]=outputs[0]*1.828422785;
|
||||
outputs[1]=sigmoid3(outputs[1]);
|
||||
|
||||
avg=outputs[1];
|
||||
if (numreq_total_adj<0.11) avg=1;//When no item's are requested, result is 0.1 ; In that case there is no deficit, so return 1 = 100% demand fulfilled
|
||||
if (avg>1) avg=1;
|
||||
if (avg<0) avg=0;
|
||||
|
||||
return [sigmoid0(outputs[0]),avg];
|
||||
}
|
||||
|
||||
module.exports = {Dose,Tick}
|
||||
@@ -15,9 +15,8 @@ const child_process = require("child_process");
|
||||
const path = require("path");
|
||||
|
||||
const mkdirp = require("mkdirp");
|
||||
const appRootPath = require("app-root-path");
|
||||
|
||||
const fileOps = require("_app/fileOps");
|
||||
const fileOps = require("lib/fileOps");
|
||||
|
||||
module.exports = class factorioServer {
|
||||
constructor({
|
||||
@@ -165,4 +164,4 @@ module.exports = class factorioServer {
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const appRootPath = require("app-root-path");
|
||||
const parallel = require("mocha.parallel");
|
||||
const mkdirp = require("mkdirp");
|
||||
|
||||
@@ -75,7 +74,7 @@ describe("class factorioServer/index.js", ()=>{
|
||||
describe(".createMap(mapName.zip)", ()=>{
|
||||
it("creates a map file as instanceDirectory/saves/map.zip", async function(){
|
||||
this.timeout(20000); // these test often takes a lot of time in the map creation step (4500 ms on i3 dev system)
|
||||
let relativePath = __dirname.replace(appRootPath, "").substr(1);
|
||||
let relativePath = path.relative('.', __dirname);
|
||||
let server = new factorioServer({
|
||||
instanceDirectory: path.join(relativePath, "_testData_createMap"),
|
||||
instanceName: "Unit testing instance",
|
||||
@@ -93,7 +92,7 @@ describe("class factorioServer/index.js", ()=>{
|
||||
});
|
||||
it("supports custom filenames", async function(){
|
||||
this.timeout(20000);
|
||||
let relativePath = __dirname.replace(appRootPath, "").substr(1);
|
||||
let relativePath = path.relative('.', __dirname);
|
||||
let server = new factorioServer({
|
||||
instanceDirectory: path.join(relativePath, "_testData_createMap_2"),
|
||||
instanceName: "Unit testing instance",
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,7 +1,7 @@
|
||||
var assert = require("assert");
|
||||
var fs = require("fs");
|
||||
|
||||
var fileOps = require("_app/fileOps");
|
||||
var fileOps = require("lib/fileOps");
|
||||
var generateSSLcert = require("./generateSSLcert.js")
|
||||
|
||||
describe("generateSSLcert.js(options)", ()=>{
|
||||
|
||||
+14
-12
@@ -1,15 +1,17 @@
|
||||
const fileOps = require("_app/fileOps");
|
||||
const config = require("./../config");
|
||||
const fileOps = require("lib/fileOps");
|
||||
// const config = require("./../config");
|
||||
|
||||
module.exports = {
|
||||
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;
|
||||
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;
|
||||
},
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves to the SHA1 hash of the file given by path
|
||||
*/
|
||||
exports.hashFile = function(path) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
let hasher = crypto.createHash('sha1')
|
||||
hasher.setEncoding('hex')
|
||||
hasher.on('finish', function() {
|
||||
resolve(hasher.read());
|
||||
});
|
||||
|
||||
let stream = fs.createReadStream(path);
|
||||
stream.on('error', function(error) {
|
||||
// the docs doesn't say anything about what's passed on error
|
||||
reject(error);
|
||||
});
|
||||
stream.pipe(hasher);
|
||||
});
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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");
|
||||
+230
-226
@@ -3,20 +3,18 @@ Provides tools to manage mods in a clusterio install
|
||||
@module modManager
|
||||
*/
|
||||
|
||||
const rightpad = require("right-pad");
|
||||
const asTable = require("as-table").configure({delimiter: ' | '});
|
||||
const moment = require("moment");
|
||||
const syncRequest = require("sync-request");
|
||||
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 fileOps = require("lib/fileOps");
|
||||
// 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;
|
||||
@@ -24,242 +22,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){
|
||||
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()))
|
||||
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"
|
||||
|
||||
@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
|
||||
*/
|
||||
});
|
||||
}
|
||||
/**
|
||||
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
|
||||
*/
|
||||
module.exports.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));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports.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();
|
||||
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 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
|
||||
} 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));
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// treat it as a search and download the top result
|
||||
factorioAPI.init(false, modDirectory, "", config.factorio_version);
|
||||
|
||||
// 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(() => {
|
||||
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();
|
||||
});
|
||||
}).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));
|
||||
});
|
||||
});
|
||||
},
|
||||
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;
|
||||
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);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
reject(new Error("Invalid search term"));
|
||||
}
|
||||
});
|
||||
}
|
||||
module.exports.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
|
||||
*/
|
||||
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;
|
||||
}
|
||||
+11
-12
@@ -56,22 +56,21 @@ function download(url, file, callback){
|
||||
}
|
||||
|
||||
var directories = [
|
||||
"database/linvodb",
|
||||
"sharedMods",
|
||||
"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,5 @@
|
||||
{
|
||||
"name": "lib",
|
||||
"description": "Internal library for factorioClusterio"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
--[[
|
||||
|
||||
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', '0.17.50', [===[
|
||||
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.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 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.6', [===[
|
||||
--[[
|
||||
|
||||
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 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 left = mod_gui.get_frame_flow(player)
|
||||
|
||||
if e.element.name ~= 'hotpatch-menu' then
|
||||
local menu = left['hotpatch-menu']
|
||||
menu.visible = not menu.visible
|
||||
on_gui_click_handlers[e.element.name](e)
|
||||
end
|
||||
end,
|
||||
['hotpatch-menu.IDE'] = function(e)
|
||||
local player = game.players[e.player_index]
|
||||
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 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 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 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 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 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)
|
||||
|
||||
--]===])
|
||||
|
||||
--[===[
|
||||
-- keeping this as a reminder
|
||||
['hotpatch-menu'] = function(e)
|
||||
local player = game.players[e.player_index]
|
||||
local top = mod_gui.get_button_flow(player)
|
||||
local left = mod_gui.get_frame_flow(player)
|
||||
local center = player.gui.center
|
||||
|
||||
local menu = left['hotpatch-menu']
|
||||
menu.visible = not menu.visible
|
||||
|
||||
on_gui_click_handlers[e.element.name](e)
|
||||
end,
|
||||
--]===]
|
||||
|
||||
return true
|
||||
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.3', [===[
|
||||
--[[
|
||||
|
||||
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 ruimte in te stuur. Doen dit deur 'n vuurpyl te bou en hom te lanseer. Jy sal baie gevorderde tegnologiëe moet navors om die Vuurpyl Silo te ontsluit. Begin klein, werk jou pad op met outomatisering en moenie vergeet om jouself te beskerm teen die inboorlinge nie.
|
||||
scenario-name=Vryspel
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=.بكوكلا اذه ناكس م كسفن يمحت نا ىسنت الو ,ةيكيتاموتوألا ربع كقيرط ينبتسو ,رفصلا نم أدبتس .كلت خيراوصلا قالطإ ةطحم حتف عيطتست فيكل ةروطتملا ايجولونكتلل ثاحبأب مايقلا ىلإ جاتحتس .اهنم خوراص قالطإ مث خيراوص قالطإ ةطحم ءانب قيرط نع كلذب مايقلا كنكمي ,ءاضفلا ىلإ اخوراص قلطت نا يه كتمهم ,factorioـل رحلا بعللا روط وه اذه
|
||||
scenario-name=رحلا بعللا
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Гэта рэжым свабоднай гульні Factorio. Вашая мэта - запусціць ракету ў космас. Дзеля гэтага вам трэба пабудаваць ракетную шахту і саму ракету са спадарожнікам. Вы мусіце даследаваць перадавыя тэхналогіі, каб навучыцца будаваць ракетную шахту. Пачынайце з малога, працуйце над аўтаматызацыяй вытворчасці, даследуйце новыя тэхналогіі і не забывайцеся на абарону ад «тутэйшых».
|
||||
scenario-name=Свабодная гульня
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Това е режимът на свободна игра. Задачата ви е да изстреляте ракета в космоса. За това ще е нужно да построите ракетна шахта и изстреляте ракетата със спътник. Ще трябва да проучите развити технологии, които ще отключат ракетната шахта. Започнете с малко и постепенно автоматизирайте процеса, като не забравяте да се защитавате от местните форми на живот.
|
||||
scenario-name=Свободна игра
|
||||
|
||||
@@ -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. Necessitareu investigar tecnologies avançades per poder-ho fer. Aneu pas a pas, avanceu cap a l'automatització i no oblideu protegir-vos dels natius.
|
||||
scenario-name=Mode lliure
|
||||
|
||||
@@ -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. Pro odemknutí raketového sila budeš muset vyzkoumat pokročilé technologie. Začni v malém, postupuj s automatizací a nezapomeň se chránit před domorodci.
|
||||
scenario-name=Volná hra
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Dette er Factorio freeplay. Din opgave er at affyre en raket ud i rummet. Gør dette ved at konstruere en raketsilo og affyr en raket. Du bliver nød til at undersøge avancerede teknologier for at låse op for raketsiloen. Start småt og arbejd din vej op med automatisering og glem ikke at beskytte dig selv fra de indfødte.
|
||||
scenario-name=Fri lej
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Du befindest dich im freien Spiel Modus von Factorio. Ziel ist es, eine Rakete ins Weltall zu schicken. Baue hierzu das Raketensilo um die Rakete starten zu können. Um das Raketensilo freischalten zu können wirst du fortgeschrittene Technologien erforschen müssen. Fange klein an und erforsche die Automation. Vergiss aber nicht, dich vor den Ureinwohner zu schützen.
|
||||
scenario-name=Freies Spiel
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Αυτό είναι το ελεύθερο παιχνίδι του Factorio. Ο στόχος σας είναι να εκτοξεύσετε έναν πύραυλο στο διάστημα. Αυτό το κάνετε κατασκευάζοντας ένα σιλό πυραύλου και εκτοξεύοντας έναν πύραυλο. Θα πρέπει να ερευνήσετε προηγμένες τεχνολογίες για να ξεκλειδώσετε το σιλό πυραύλου. Ξεκινήστε χαμηλά, φτάστε ψηλά χρησιμοποιώντας αυτοματισμούς και μην ξεχάσετε να προστατέψετε τον εαυτό σας από τους ντόπιους.
|
||||
scenario-name=Ελεύθερο παιχνίδι
|
||||
|
||||
@@ -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. You will need to research advanced technologies in order to unlock the rocket silo. Start small, work your way up with automation and don't forget to protect yourself from the natives.
|
||||
scenario-name=Freeplay
|
||||
@@ -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,2 @@
|
||||
scenario-name=Libera Ludado
|
||||
|
||||
@@ -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.
|
||||
scenario-name=Modo libre
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
scenario-name=Vabamäng
|
||||
|
||||
@@ -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.
|
||||
scenario-name=Vapaapeli
|
||||
|
||||
@@ -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. Faites ceci en construisant une fusée et en la lançant dans l'espace. Vous aurez besoin de rechercher des technologies de pointe afin de débloquer le silo. Commencez petit, évoluez en automatisant et n'oubliez pas de vous protéger contre les autochtones.
|
||||
scenario-name=Jeu libre
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=.םיימוקמה םירוציה ינפמ םכמצע לע ןגהל וחכשת לאו ,יטמוטוא ןוכימ תועצמאב םכלש הישעתה תא וביחרה ,ןטקב וליחתה .ןקתמה תיינב תא רשפאל ידכ תומדקתמ תויגולונכט רוקחל םכילע היהי .ליט ותועצמאב רגשלו םיליט רוגיש ןקתמ תונבל םכילע .ללחל ליט רגשל איה םכתמישמ .ויִרֹוטְקַפ לש ישפוחה שיחרתה והז
|
||||
scenario-name=ישפוח קחשמ
|
||||
|
||||
@@ -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.
|
||||
scenario-name=Szabad játék
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Questa è la modalità libera di Factorio. Il tuo obiettivo è quello di lanciare un razzo nello spazio e per farlo dovrai costruire un silo per razzi. Avrai bisogno di fare ricerche avanzate per sbloccare il silo. Inizia con poco, lavora sull'automazione e non dimenticarti di difenderti dai nativi.
|
||||
scenario-name=Gioco libero
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=これは、Factorioのフリープレイです。ロケットサイロを建造し、ロケットを宇宙へ打ち上げるのがあなたの任務です。ロケットサイロの建造を行うためには高度な技術研究を行う必要があります。小規模に始め、自動化しつつ進み、原住生物から身を守ることも忘れないでください。
|
||||
scenario-name=フリープレイ
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Factorio 자유플레이입니다. 당신의 목표는 우주로 로켓을 발사하는 것입니다. 로켓 격납고를 건설하고 로켓을 발사하여 이 목표를 달성하세요. 로켓 격납고의 잠금을 해제하기 위해 고급 기술들을 연구할 필요가 있습니다. 작은 것부터 시작해 자동화로 차근차근 발전하고, 토착 생물로부터 자신을 보호하는 것도 잊지 마세요.
|
||||
scenario-name=자유플레이
|
||||
|
||||
@@ -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ų.
|
||||
scenario-name=Žaisti laisva forma
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
scenario-name=Brīvspēle
|
||||
|
||||
@@ -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. Je zult geavanceerde technologieën moeten onderzoeken om de raket-silo vrij te spelen. Begin op kleine schaal, breidt uit en automatiseer je fabriek en vergeet niet jezelf te beschermen tegen de inheemse bewoners.
|
||||
scenario-name=Freeplay
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Dette er Factorio freeplay. Din oppgave er å sende en rakett til verdensrommet. Bygg en rakettsilo og send opp en rakett med en satellitt. Du blir nødt til å utforske avanserte teknologier for å få tilgang til rakettsiloen. Begynn i det små, jobb deg oppover med hjelp av automatisering, men ikke glem å beskytte deg mot de innfødte.
|
||||
scenario-name=Frittspill
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Oto swobodna rozgrywka Factorio. Twoim zadaniem jest wysłanie rakiety w kosmos. Dokonaj tego przez skonstruowanie silosu rakietowego i wystrzelenie rakiety. Żeby odblokować silos rakietowy będziesz musiał odkryć zaawansowane technologie. Rozpocznij od podstaw, rozwijaj się wykorzystując automatyzację i nie zapomnij o obronie przed rodzimymi mieszkańcami planety.
|
||||
scenario-name=Gra swobodna
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Este é o modo livre de Factorio. Sua tarefa é lançar um foguete no espaço. Faça isso construindo um silo de foguetes e lançando um foguete. Você precisará pesquisar tecnologias avançadas para desbloquear o silo de foguetes. Comece pequeno, trabalhe seu caminho com automação e não se esqueça de se proteger dos nativos.
|
||||
scenario-name=Jogo livre
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Este é o modo livre do Factorio. O teu objetivo é lançar um foguetão para o espaço. Faz isto, contruindo um silo de foguetão e lançando-o.\nVais precisar de pesquisar tecnologias avançadas para desploquear o silo de foguetão. Começa simples, e trabalha e evolui com automação mas não te esqueças de proteger do nativos.
|
||||
scenario-name=Modo Livre
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Acesta este modul Factorio freeplay. Sarcina ta este de a lansa o racheta în spațiu. Fă acest lucru prin construirea unui siloz de rachete și lansarea unei rachete. Va trebui să cercetezi tehnologiile avansate pentru a debloca silozul de rachete. Începe mic, avansează cu automatizarea și nu uita să te protejezi de nativi.
|
||||
scenario-name=Freeplay
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Это режим свободной игры Factorio. Ваша задача - запустить ракету в космос. Для её запуска потребуется исследовать новые технологии и создать ракетную шахту. Начните с малого, работайте над автоматизацией производства и не забывайте о защите от местных форм жизни.
|
||||
scenario-name=Свободная игра
|
||||
|
||||
@@ -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. Pre odomknutie raketového sila budeš musieť vyskúmať pokročilé technológie. Začni v malom, postupuj s automatizáciou a nezabnudni sa chrániť pred domácimi.
|
||||
scenario-name=Voľná hra
|
||||
|
||||
@@ -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.
|
||||
scenario-name=Prosta igra
|
||||
|
||||
@@ -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.
|
||||
scenario-name=Frispel
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
scenario-name=โหมดอิสระ
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Bu Factorio'nun serbest oynanışıdır. Göreviniz uzaya bir roket göndermek. Bunu roket rampası inşa edip ve Roket fırlatarak yapınız. Roket rampasını açmak için gelişmiş teknolojileri araştırmanız gerekecek. Ufaktan başlayın ve otomasyonla büyüyün, ve yerlilerden kendiniz korumayı unutmayın.
|
||||
scenario-name=Serbest Oyun
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Це один з типів гри в Factorio — "Вільна гра". Ваше завдання — запустити ракету у космічний простір. Зробіть це, побудувавши ракетну шахту та запустивши ракету. Вам потрібно дослідити передові технології, щоб мати змогу побудувати ракетну шахту. Починайте з малого, розвивайтеся за допомогою автоматизації, але не забувайте і про оборону.
|
||||
scenario-name=Вільна гра
|
||||
|
||||
Loaded 100 of 3818 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user