mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
Compare commits
446
Commits
1.2.x
...
v2.0.0-alpha.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d96fb75703 | ||
|
|
20c7b613f2 | ||
|
|
59436e7f10 | ||
|
|
a614ef8405 | ||
|
|
6d005505ce | ||
|
|
ceade431ef | ||
|
|
8586dfb95f | ||
|
|
00a1f6aea7 | ||
|
|
e5e82ba2fb | ||
|
|
24397b75cc | ||
|
|
fbbaf3dd3a | ||
|
|
a84f2d5eb6 | ||
|
|
ee81fbb7f6 | ||
|
|
65f0845398 | ||
|
|
7968d89345 | ||
|
|
0ae520b5cc | ||
|
|
1b0b245484 | ||
|
|
c5a14db8ea | ||
|
|
7bba6cbde6 | ||
|
|
b2408d54ec | ||
|
|
f06d3509e6 | ||
|
|
18e609f3fe | ||
|
|
2a7bc75929 | ||
|
|
34a0ce4dc3 | ||
|
|
003b035abd | ||
|
|
b82de23876 | ||
|
|
363bb54902 | ||
|
|
dfaf74aac5 | ||
|
|
86b8775df0 | ||
|
|
bccc89fa0b | ||
|
|
03b7d22ba6 | ||
|
|
8dce1dd0fd | ||
|
|
94200433cc | ||
|
|
392a7655c4 | ||
|
|
f1f9b4d020 | ||
|
|
701ff87bcd | ||
|
|
2382f4bbdc | ||
|
|
f318f8826b | ||
|
|
fbf2843f5b | ||
|
|
bca949f7be | ||
|
|
5e410a6ac3 | ||
|
|
0cca9ed22c | ||
|
|
6621e37e23 | ||
|
|
bd8f685a18 | ||
|
|
3485c9ac68 | ||
|
|
bf5a4a8a9c | ||
|
|
198d5c344b | ||
|
|
efa3eff0d5 | ||
|
|
6fde461da5 | ||
|
|
14ddb76a0b | ||
|
|
3753c303dd | ||
|
|
08ec542907 | ||
|
|
a8b8232d73 | ||
|
|
4f86be1b7f | ||
|
|
84450b83b4 | ||
|
|
f08774bd4a | ||
|
|
5e5a47ce02 | ||
|
|
9761fbb71c | ||
|
|
3c55188a87 | ||
|
|
a40c78d85d | ||
|
|
0b790357f9 | ||
|
|
2dcc5c9d47 | ||
|
|
6d8d926268 | ||
|
|
c59c30b840 | ||
|
|
fd307e58c2 | ||
|
|
da1cbc16f7 | ||
|
|
9b870fde64 | ||
|
|
0802c5b29d | ||
|
|
781e40428f | ||
|
|
999386c088 | ||
|
|
8c6130adba | ||
|
|
9458155a1b | ||
|
|
0969e07d79 | ||
|
|
9c9b3ac625 | ||
|
|
7814656241 | ||
|
|
e9f5cae4f0 | ||
|
|
8cb21c5fa8 | ||
|
|
3e4f0b6b5e | ||
|
|
703fda43ad | ||
|
|
3dba92a03d | ||
|
|
b254e4189d | ||
|
|
29f066a191 | ||
|
|
34aaeee354 | ||
|
|
efd759bec5 | ||
|
|
922c6af632 | ||
|
|
cf6f2cb70a | ||
|
|
2acf57d23d | ||
|
|
07cff0ab9a | ||
|
|
3dfed4d2ad | ||
|
|
c3179f0423 | ||
|
|
37bff10382 | ||
|
|
931ad4816f | ||
|
|
5b043524b4 | ||
|
|
8c9daff0bb | ||
|
|
67ca8dcc4d | ||
|
|
fc3686b865 | ||
|
|
01ea301a8c | ||
|
|
36cf14074e | ||
|
|
f3d4cfdcf5 | ||
|
|
f8597f500c | ||
|
|
2b20c943bb | ||
|
|
1bddea8cec | ||
|
|
e2255ddb7c | ||
|
|
453226f246 | ||
|
|
260f976fa3 | ||
|
|
d1b851d890 | ||
|
|
2e24d6c8ae | ||
|
|
203f2d2a14 | ||
|
|
87d14252fc | ||
|
|
3211fbe985 | ||
|
|
a8e29fdc56 | ||
|
|
a194508f52 | ||
|
|
ca2839594e | ||
|
|
1b427af5f0 | ||
|
|
04ee3d3f96 | ||
|
|
c87538ca58 | ||
|
|
c35b6de70e | ||
|
|
b8b8c44083 | ||
|
|
1beffb1bfd | ||
|
|
b151dd8469 | ||
|
|
7a9b6592e6 | ||
|
|
5c1214ad38 | ||
|
|
1629eaf225 | ||
|
|
4060c75142 | ||
|
|
f3c13eab21 | ||
|
|
e9fab3cc10 | ||
|
|
446333f4aa | ||
|
|
f8817ecf0c | ||
|
|
c601c53044 | ||
|
|
3603b16901 | ||
|
|
f4ca8366e2 | ||
|
|
b6585e6364 | ||
|
|
d2fb33f8b9 | ||
|
|
568cf81204 | ||
|
|
15ad64e356 | ||
|
|
ef6441ca46 | ||
|
|
534408856c | ||
|
|
430aa6d207 | ||
|
|
00d1d304d2 | ||
|
|
38c392ff6d | ||
|
|
4f463a8180 | ||
|
|
2c855a5846 | ||
|
|
61d92a6910 | ||
|
|
9507608e59 | ||
|
|
47ea8f5477 | ||
|
|
e7d1039942 | ||
|
|
fc04fd9ebc | ||
|
|
3dc4bdaab6 | ||
|
|
59111cc844 | ||
|
|
d537e5cf6b | ||
|
|
47c221f6f3 | ||
|
|
56488b2c2d | ||
|
|
e908c5069b | ||
|
|
139585f8c6 | ||
|
|
6bc07ad1b3 | ||
|
|
56ee1fe955 | ||
|
|
62e38199bd | ||
|
|
407e3d2aed | ||
|
|
fcfad08260 | ||
|
|
112cad8a49 | ||
|
|
ccb07d6eab | ||
|
|
621c834126 | ||
|
|
75cd6d38db | ||
|
|
99ac8f4f33 | ||
|
|
75067e992d | ||
|
|
31b66339a0 | ||
|
|
8ebf0fc754 | ||
|
|
5e9d9097e1 | ||
|
|
3343d53d75 | ||
|
|
541d2ad337 | ||
|
|
59758f1576 | ||
|
|
706ffd9ed2 | ||
|
|
17973e767e | ||
|
|
5c16809b5b | ||
|
|
ee063300ed | ||
|
|
3cc84d6400 | ||
|
|
65494a0a91 | ||
|
|
f05100c94d | ||
|
|
9a4a535add | ||
|
|
3ff5e630ca | ||
|
|
c2df80a17f | ||
|
|
f9f0e5b5f7 | ||
|
|
028f00e58a | ||
|
|
ae86781b0d | ||
|
|
4c26fcee3a | ||
|
|
d4e072f376 | ||
|
|
026b7f1306 | ||
|
|
8de3f54bc4 | ||
|
|
43ce6aa58c | ||
|
|
0a9fff7578 | ||
|
|
b73f2f6006 | ||
|
|
277b78bf6d | ||
|
|
1aef3c9609 | ||
|
|
9f74d7b2ec | ||
|
|
7aa340675b | ||
|
|
27ef00705b | ||
|
|
38c60c221f | ||
|
|
677512512e | ||
|
|
808bd6b453 | ||
|
|
941ceab935 | ||
|
|
9dac57b36b | ||
|
|
23be732a5c | ||
|
|
737c9924b9 | ||
|
|
d9fe408c42 | ||
|
|
090fbd6dff | ||
|
|
cb1d744060 | ||
|
|
ddd2027739 | ||
|
|
6a8346186f | ||
|
|
5681ef9733 | ||
|
|
6f22942cc5 | ||
|
|
1e6b807ae3 | ||
|
|
5ef6d02054 | ||
|
|
eac17b4340 | ||
|
|
54224755e9 | ||
|
|
557d380112 | ||
|
|
5e5149dc93 | ||
|
|
370ee9ed04 | ||
|
|
f4a0b88ff6 | ||
|
|
e137547664 | ||
|
|
d5c43cc3d6 | ||
|
|
459c039f17 | ||
|
|
892c803496 | ||
|
|
b8fb73f863 | ||
|
|
6cf16cc09e | ||
|
|
0e9cd35c30 | ||
|
|
66aa7e1e43 | ||
|
|
221dc855f4 | ||
|
|
f88e1f1d3a | ||
|
|
c091dc341e | ||
|
|
e89e54ee27 | ||
|
|
80bd75ba16 | ||
|
|
11992d1839 | ||
|
|
610859eb3b | ||
|
|
37d8eedc0d | ||
|
|
9d2e89ec63 | ||
|
|
1b61d09a07 | ||
|
|
ac3aa5ffee | ||
|
|
b2a902f784 | ||
|
|
f700e98e8e | ||
|
|
770c961900 | ||
|
|
1873f9bda8 | ||
|
|
26eefb0476 | ||
|
|
8229d1955c | ||
|
|
b4c7824101 | ||
|
|
1f6574a89c | ||
|
|
8b9bb59357 | ||
|
|
b71c392b32 | ||
|
|
bcceae687f | ||
|
|
513651393b | ||
|
|
e5115df28b | ||
|
|
21d9fdff7b | ||
|
|
22c325394f | ||
|
|
ca21ff741b | ||
|
|
d4dd290654 | ||
|
|
f0afbf0a33 | ||
|
|
5dd1490c75 | ||
|
|
9c8ae2bf06 | ||
|
|
5d8fa4e4ce | ||
|
|
9f24d200b7 | ||
|
|
47cff048f0 | ||
|
|
b24778dce6 | ||
|
|
62afcaadab | ||
|
|
b59fd80a7b | ||
|
|
7a03b84858 | ||
|
|
a2d10c8c02 | ||
|
|
3e7f497669 | ||
|
|
f1ac41bc4c | ||
|
|
b2239aeb96 | ||
|
|
34d4604bab | ||
|
|
ea4f3ab2bf | ||
|
|
c6fd46e3c3 | ||
|
|
3be264c9a9 | ||
|
|
b24c129e38 | ||
|
|
f5d2b058d9 | ||
|
|
3f86011db1 | ||
|
|
f8aa947a98 | ||
|
|
bfff297d9a | ||
|
|
3bccbac496 | ||
|
|
a3eb39712c | ||
|
|
039b41d3e6 | ||
|
|
278dc9b2d2 | ||
|
|
2d3307c045 | ||
|
|
5228e0a4d7 | ||
|
|
d9910b0750 | ||
|
|
1dea5fddbe | ||
|
|
f2c3006046 | ||
|
|
554cab4fb0 | ||
|
|
7a2abaa5db | ||
|
|
5858d97857 | ||
|
|
bc41f7afa6 | ||
|
|
65f75d86e6 | ||
|
|
fcae3ef0c1 | ||
|
|
7b2ba58701 | ||
|
|
27fc68da0f | ||
|
|
ca2967e255 | ||
|
|
d38e9a53c4 | ||
|
|
d2d058e7d6 | ||
|
|
2f3b4c1aa8 | ||
|
|
f3171c2feb | ||
|
|
1d26212e1d | ||
|
|
0474355175 | ||
|
|
ba7626ff2d | ||
|
|
707f06be76 | ||
|
|
35f3f4f02b | ||
|
|
31ab3a09f6 | ||
|
|
7c79ea44b6 | ||
|
|
7359f9075e | ||
|
|
f4b2300d9b | ||
|
|
680d196584 | ||
|
|
bdf8cd1152 | ||
|
|
246a8031c1 | ||
|
|
a781c2f988 | ||
|
|
9082b40abd | ||
|
|
f163e6a39b | ||
|
|
283d47d88b | ||
|
|
be02e57012 | ||
|
|
cb8d7e240f | ||
|
|
6f5534c45b | ||
|
|
467249579d | ||
|
|
3fdc27238b | ||
|
|
72b16de227 | ||
|
|
9a2b2c9f8d | ||
|
|
e4aae88614 | ||
|
|
bc444b79c2 | ||
|
|
ff3fc3b94b | ||
|
|
6bde8a2219 | ||
|
|
a9a382b993 | ||
|
|
e287ca6ea9 | ||
|
|
5d9d3981ff | ||
|
|
1595555154 | ||
|
|
1a035e849e | ||
|
|
079e31cf05 | ||
|
|
e6d995d97a | ||
|
|
c08d97e698 | ||
|
|
4d94fb9a6c | ||
|
|
93480c8b29 | ||
|
|
a01b971ba2 | ||
|
|
6b4ab6660c | ||
|
|
33e9f9d983 | ||
|
|
cbcf868c9e | ||
|
|
6a870be233 | ||
|
|
2ff52a1cf2 | ||
|
|
3202e2d150 | ||
|
|
debb33eb3f | ||
|
|
34fc6f0820 | ||
|
|
e8d9d160c4 | ||
|
|
999fb79570 | ||
|
|
77571d3c71 | ||
|
|
de20635aea | ||
|
|
5d24329456 | ||
|
|
4eda983cf4 | ||
|
|
7f82680511 | ||
|
|
9098b3cf19 | ||
|
|
183902ced3 | ||
|
|
b3eae22221 | ||
|
|
0b3e4fe6f4 | ||
|
|
7d1942b615 | ||
|
|
9eb9291314 | ||
|
|
b068550775 | ||
|
|
6726c2b16f | ||
|
|
706a1dbd39 | ||
|
|
a62e8d17a3 | ||
|
|
a4983d75fb | ||
|
|
1fd8ae954b | ||
|
|
e15462439a | ||
|
|
312c45bda5 | ||
|
|
aaf43143df | ||
|
|
c5cae30d83 | ||
|
|
a783bd9651 | ||
|
|
5f33cf4d3b | ||
|
|
dd865b42b5 | ||
|
|
858449c57a | ||
|
|
53a5ce062b | ||
|
|
1ba9e68603 | ||
|
|
ec223e9d9e | ||
|
|
a63c341209 | ||
|
|
74c99b9e13 | ||
|
|
bcb22d2af7 | ||
|
|
eb1b863cec | ||
|
|
2428cba2bd | ||
|
|
500c9584eb | ||
|
|
18a904d9de | ||
|
|
85daf6df19 | ||
|
|
a57b0a3b5e | ||
|
|
8aa66e9e68 | ||
|
|
d378287941 | ||
|
|
ea5c0d5aa3 | ||
|
|
094d44a7f5 | ||
|
|
1bbf1542d2 | ||
|
|
98900e72dc | ||
|
|
da99b39402 | ||
|
|
aba22960a6 | ||
|
|
0ec37414c2 | ||
|
|
2e50921d32 | ||
|
|
e14f7c22fd | ||
|
|
a31778c7ff | ||
|
|
35546b4b18 | ||
|
|
651b09b613 | ||
|
|
e1263a40b1 | ||
|
|
23ce86aef6 | ||
|
|
5d6f6de379 | ||
|
|
af2471b427 | ||
|
|
8c1c8d07a9 | ||
|
|
b0ad02d412 | ||
|
|
7d61cd9716 | ||
|
|
31298c7c1c | ||
|
|
b191f7d831 | ||
|
|
f4ca5d92a1 | ||
|
|
703423ae54 | ||
|
|
84f48e36f4 | ||
|
|
6ee84c41bf | ||
|
|
ef66ab37da | ||
|
|
b73b202513 | ||
|
|
5d28718175 | ||
|
|
3b08b73c37 | ||
|
|
9c9b4463a4 | ||
|
|
fb2519cb50 | ||
|
|
5a9e2a8825 | ||
|
|
a39a17b220 | ||
|
|
940a7b9180 | ||
|
|
ab2a377ca5 | ||
|
|
ea2f291025 | ||
|
|
3e80e40223 | ||
|
|
ed54db0db9 | ||
|
|
7dcd46a99a | ||
|
|
0d4ad2f075 | ||
|
|
0a9ddef120 | ||
|
|
38462d3069 | ||
|
|
0398bd10f8 | ||
|
|
55164cc48f | ||
|
|
44b7d0aa19 | ||
|
|
cc87654742 | ||
|
|
ec7a93c14f | ||
|
|
ca03bed8b7 | ||
|
|
88739a952a | ||
|
|
c645fac3de | ||
|
|
c1699fe33d | ||
|
|
69a1ff7817 | ||
|
|
e4386d57e8 | ||
|
|
bfa8a99daa | ||
|
|
d72bcbdf01 | ||
|
|
71c07fa527 | ||
|
|
8898e923de | ||
|
|
a3fc1dda8c | ||
|
|
a828f50e57 | ||
|
|
442472e412 |
No files matched your search
@@ -0,0 +1 @@
|
||||
packages/master/static
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
"use strict";
|
||||
module.exports = {
|
||||
"env": {
|
||||
"node": true,
|
||||
"commonjs": true,
|
||||
"es6": true,
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 11,
|
||||
},
|
||||
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["test/**/*.js", "plugins/*/test/**/*.js"],
|
||||
"env": {
|
||||
"mocha": "true",
|
||||
},
|
||||
"rules": {
|
||||
"prefer-arrow-callback": "off",
|
||||
"no-invalid-this": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
"files": ["packages/master/web/**/*.jsx"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
"rules": {
|
||||
"accessor-pairs": "error",
|
||||
"array-bracket-newline": "off",
|
||||
"array-bracket-spacing": ["error", "never"],
|
||||
"array-callback-return": "error",
|
||||
"array-element-newline": "off",
|
||||
"arrow-body-style": "error",
|
||||
"arrow-parens": "off",
|
||||
"arrow-spacing": ["error", { "after": true, "before": true }],
|
||||
"block-scoped-var": "error",
|
||||
"block-spacing": "error",
|
||||
"brace-style": ["warn", "1tbs", { "allowSingleLine": true }],
|
||||
"callback-return": "off",
|
||||
"camelcase": "off",
|
||||
"capitalized-comments": "off",
|
||||
"class-methods-use-this": "off",
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
{
|
||||
"arrays": "always-multiline",
|
||||
"objects": "always-multiline",
|
||||
"imports": "always-multiline",
|
||||
"exports": "always-multiline",
|
||||
"functions": "only-multiline",
|
||||
},
|
||||
],
|
||||
"comma-spacing": "warn",
|
||||
"comma-style": ["error", "last"],
|
||||
"complexity": "warn",
|
||||
"computed-property-spacing": ["error", "never"],
|
||||
"consistent-return": "error",
|
||||
"consistent-this": "error",
|
||||
"curly": "warn",
|
||||
"default-case": "error",
|
||||
"default-case-last": "error",
|
||||
"default-param-last": "error",
|
||||
"dot-location": ["error", "property"],
|
||||
"dot-notation": ["warn", { "allowKeywords": true }],
|
||||
"eol-last": "error",
|
||||
"eqeqeq": "error",
|
||||
"func-call-spacing": "error",
|
||||
"func-name-matching": "error",
|
||||
"func-names": "off",
|
||||
"func-style": ["error", "declaration"],
|
||||
"function-call-argument-newline": "off",
|
||||
"function-paren-newline": "off",
|
||||
"generator-star-spacing": ["error", { "before": false, "after": true }],
|
||||
"global-require": "warn",
|
||||
"grouped-accessor-pairs": "error",
|
||||
"guard-for-in": "error",
|
||||
"handle-callback-err": "error",
|
||||
"id-blacklist": "error",
|
||||
"id-length": "off",
|
||||
"id-match": "error",
|
||||
"implicit-arrow-linebreak": "error",
|
||||
"indent": ["error", "tab", { "SwitchCase": 1 }],
|
||||
"indent-legacy": "off",
|
||||
"init-declarations": "off",
|
||||
"jsx-quotes": "error",
|
||||
"key-spacing": "error",
|
||||
"keyword-spacing": "warn",
|
||||
"line-comment-position": "off",
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"lines-around-comment": "off",
|
||||
"lines-around-directive": "off",
|
||||
"lines-between-class-members": ["error", "always"],
|
||||
"max-classes-per-file": "off",
|
||||
"max-depth": "warn",
|
||||
"max-len": ["warn", { "code": 120 }],
|
||||
"max-lines": "off",
|
||||
"max-lines-per-function": "off",
|
||||
"max-nested-callbacks": "error",
|
||||
"max-params": "off",
|
||||
"max-statements": "off",
|
||||
"max-statements-per-line": "off",
|
||||
"multiline-comment-style": ["warn", "separate-lines"],
|
||||
"multiline-ternary": "off",
|
||||
"new-cap": "error",
|
||||
"new-parens": "error",
|
||||
"newline-after-var": "off",
|
||||
"newline-before-return": "off",
|
||||
"newline-per-chained-call": "off",
|
||||
"no-alert": "error",
|
||||
"no-array-constructor": "error",
|
||||
"no-await-in-loop": "off",
|
||||
"no-bitwise": "off",
|
||||
"no-buffer-constructor": "error",
|
||||
"no-caller": "error",
|
||||
"no-catch-shadow": "error",
|
||||
"no-confusing-arrow": "error",
|
||||
"no-console": "off",
|
||||
"no-constant-condition": ["error", { "checkLoops": false }],
|
||||
"no-constructor-return": "error",
|
||||
"no-continue": "off",
|
||||
"no-div-regex": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"no-else-return": "warn",
|
||||
"no-empty-function": "off",
|
||||
"no-eq-null": "error",
|
||||
"no-eval": "error",
|
||||
"no-extend-native": "error",
|
||||
"no-extra-bind": "error",
|
||||
"no-extra-label": "error",
|
||||
"no-extra-parens": "off",
|
||||
"no-floating-decimal": "error",
|
||||
"no-implicit-coercion": "error",
|
||||
"no-implicit-globals": "error",
|
||||
"no-implied-eval": "error",
|
||||
"no-inline-comments": "off",
|
||||
"no-invalid-this": "warn",
|
||||
"no-iterator": "error",
|
||||
"no-label-var": "error",
|
||||
"no-lone-blocks": "error",
|
||||
"no-lonely-if": "error",
|
||||
"no-loop-func": "error",
|
||||
"no-magic-numbers": "off",
|
||||
"no-mixed-operators": [
|
||||
"error",
|
||||
{
|
||||
"groups": [
|
||||
["&", "|", "^", "~", "<<", ">>", ">>>"],
|
||||
["==", "!=", "===", "!==", ">", ">=", "<", "<="],
|
||||
["in", "instanceof"],
|
||||
],
|
||||
},
|
||||
],
|
||||
"no-mixed-requires": "error",
|
||||
"no-multi-assign": "error",
|
||||
"no-multi-spaces": "error",
|
||||
"no-multi-str": "error",
|
||||
"no-multiple-empty-lines": "error",
|
||||
"no-native-reassign": "error",
|
||||
"no-negated-condition": "off",
|
||||
"no-negated-in-lhs": "error",
|
||||
"no-nested-ternary": "error",
|
||||
"no-new": "error",
|
||||
"no-new-func": "error",
|
||||
"no-new-object": "error",
|
||||
"no-new-require": "error",
|
||||
"no-new-wrappers": "error",
|
||||
"no-octal-escape": "error",
|
||||
"no-param-reassign": "off",
|
||||
"no-path-concat": "error",
|
||||
"no-plusplus": ["warn", { "allowForLoopAfterthoughts": true }],
|
||||
"no-process-env": "error",
|
||||
"no-process-exit": "warn",
|
||||
"no-proto": "error",
|
||||
"no-restricted-exports": "error",
|
||||
"no-restricted-globals": "error",
|
||||
"no-restricted-imports": "error",
|
||||
"no-restricted-modules": "error",
|
||||
"no-restricted-properties": "error",
|
||||
"no-restricted-syntax": "error",
|
||||
"no-return-assign": "error",
|
||||
"no-return-await": "off",
|
||||
"no-script-url": "error",
|
||||
"no-self-compare": "error",
|
||||
"no-sequences": "error",
|
||||
"no-shadow": ["error", { "allow": ["yargs"] }],
|
||||
"no-spaced-func": "error",
|
||||
"no-sync": "error",
|
||||
"no-tabs": ["error", { "allowIndentationTabs": true }],
|
||||
"no-template-curly-in-string": "error",
|
||||
"no-ternary": "off",
|
||||
"no-throw-literal": "error",
|
||||
"no-trailing-spaces": "error",
|
||||
"no-undef-init": "error",
|
||||
"no-undefined": "off",
|
||||
"no-unmodified-loop-condition": "error",
|
||||
"no-unneeded-ternary": "error",
|
||||
"no-unused-expressions": "error",
|
||||
"no-use-before-define": "warn",
|
||||
"no-useless-backreference": "error",
|
||||
"no-useless-call": "error",
|
||||
"no-useless-computed-key": "error",
|
||||
"no-useless-concat": "error",
|
||||
"no-useless-constructor": "error",
|
||||
"no-useless-rename": "error",
|
||||
"no-useless-return": "error",
|
||||
"no-var": "warn",
|
||||
"no-void": "error",
|
||||
"no-warning-comments": "off",
|
||||
"no-whitespace-before-property": "error",
|
||||
"nonblock-statement-body-position": "error",
|
||||
"object-curly-newline": "error",
|
||||
"object-curly-spacing": "off",
|
||||
"object-shorthand": "off",
|
||||
"one-var": "off",
|
||||
"one-var-declaration-per-line": "error",
|
||||
"operator-assignment": "off",
|
||||
"operator-linebreak": "warn",
|
||||
"padded-blocks": "off",
|
||||
"padding-line-between-statements": "error",
|
||||
"prefer-arrow-callback": "warn",
|
||||
"prefer-const": "off",
|
||||
"prefer-destructuring": "off",
|
||||
"prefer-exponentiation-operator": "error",
|
||||
"prefer-named-capture-group": "off",
|
||||
"prefer-numeric-literals": "error",
|
||||
"prefer-object-spread": "error",
|
||||
"prefer-promise-reject-errors": "error",
|
||||
"prefer-reflect": "off",
|
||||
"prefer-regex-literals": "error",
|
||||
"prefer-rest-params": "error",
|
||||
"prefer-spread": "error",
|
||||
"prefer-template": "warn",
|
||||
"quote-props": "off",
|
||||
"quotes": ["error", "double", { "avoidEscape": true }],
|
||||
"radix": ["error", "always"],
|
||||
"require-atomic-updates": "off", // XXX Not sure why this triggers on process.title = ...
|
||||
"require-await": "off",
|
||||
"require-jsdoc": "off",
|
||||
"require-unicode-regexp": "off",
|
||||
"rest-spread-spacing": ["error", "never"],
|
||||
"semi": "error",
|
||||
"semi-spacing": "error",
|
||||
"semi-style": "off",
|
||||
"sort-imports": "off",
|
||||
"sort-keys": "off",
|
||||
"sort-vars": "error",
|
||||
"space-before-blocks": "warn",
|
||||
"space-before-function-paren": "off",
|
||||
"space-in-parens": ["error", "never"],
|
||||
"space-infix-ops": "off",
|
||||
"space-unary-ops": "error",
|
||||
"spaced-comment": ["warn", "always"],
|
||||
"strict": "error",
|
||||
"switch-colon-spacing": "error",
|
||||
"symbol-description": "error",
|
||||
"template-curly-spacing": ["error", "never"],
|
||||
"template-tag-spacing": "error",
|
||||
"unicode-bom": ["error", "never"],
|
||||
"valid-jsdoc": ["error", { "requireReturn": false }],
|
||||
"vars-on-top": "warn",
|
||||
"wrap-iife": "error",
|
||||
"wrap-regex": "off",
|
||||
"yield-star-spacing": "error",
|
||||
"yoda": ["error", "never", { "exceptRange": true }],
|
||||
},
|
||||
};
|
||||
+11
-28
@@ -25,43 +25,26 @@ coverage
|
||||
build/Release
|
||||
dist/
|
||||
|
||||
node_modules/*
|
||||
# Autogenerated docs output
|
||||
/docs/api
|
||||
|
||||
node_modules
|
||||
|
||||
Factorio*
|
||||
/factorio
|
||||
/instances
|
||||
/sharedMods
|
||||
/database
|
||||
database/items.db
|
||||
config.json
|
||||
/plugin-list.json
|
||||
/config-master.json
|
||||
/config-control.json
|
||||
/config-slave.json
|
||||
package-lock.json
|
||||
secret-api-token.txt
|
||||
mod_cache.json
|
||||
static/pictures/spritesheet.js
|
||||
static/pictures/spritesheet.png
|
||||
static/external
|
||||
static/export
|
||||
saves
|
||||
|
||||
# weird pattern to ignore sharedPlugins except not ignore some core modules
|
||||
!sharedPlugins/
|
||||
sharedPlugins/*
|
||||
!sharedPlugins/inventoryImports/
|
||||
!sharedPlugins/UPSdisplay/
|
||||
!sharedPlugins/remoteMap/
|
||||
!sharedPlugins/remoteCommands/
|
||||
!sharedPlugins/researchSync/
|
||||
!sharedPlugins/globalChat/
|
||||
!sharedPlugins/clusterioMod/
|
||||
!sharedPlugins/serverManager/
|
||||
sharedPlugins/serverManager/database/*
|
||||
sharedPlugins/serverManager/config.json
|
||||
sharedPlugins/serverManager/secret-api-token.txt
|
||||
!sharedPlugins/scenario_loader/
|
||||
sharedPlugins/scenario_loader/scenarios
|
||||
!sharedPlugins/statisticsExporter/
|
||||
|
||||
# File used for marking plugins as disabled
|
||||
sharedPlugins/*/DISABLED
|
||||
/temp/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vs/
|
||||
+10
-5
@@ -1,10 +1,15 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "9"
|
||||
- 10
|
||||
before_install:
|
||||
- sudo apt-get -qq update
|
||||
- sudo apt-get install -y curl git tar
|
||||
- curl -o factorio.tar.gz -L https://www.factorio.com/get-download/latest/headless/linux64
|
||||
- tar -xvf factorio.tar.gz
|
||||
- tar -xf factorio.tar.gz
|
||||
- npm install -g codecov
|
||||
script:
|
||||
- npm run cover
|
||||
- npx lerna bootstrap --no-ci
|
||||
- node packages/lib/build_mod --source-dir packages/slave/lua/clusterio_lib --bump-patch --factorio-version 0.18 --output-dir temp/test/sharedMods
|
||||
- curl -o temp/test/sharedMods/subspace_storage_1.99.1.zip -L https://github.com/clusterio/factorioClusterioMod/releases/download/1.99.1/subspace_storage_1.99.1.zip
|
||||
- npm run-script ci-cover
|
||||
- cat temp/test/integration/log.txt
|
||||
- codecov -f coverage/lcov.info
|
||||
- npm run-script lint
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"CurrentProjectSetting": null
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"ExpandedNodes": [
|
||||
"",
|
||||
"\\sharedPlugins"
|
||||
],
|
||||
"SelectedNode": "\\.gitignore",
|
||||
"PreviewInSolutionExplorer": false
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+244
@@ -0,0 +1,244 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
Version 2.0.0
|
||||
-------------
|
||||
|
||||
### Major Features
|
||||
|
||||
- Replaced Hotpatch with save patching. Removed the Hotpatch scenario and
|
||||
the depency on it for getting code into the game. Added a save patcher than
|
||||
runs before starting up Factorio that patches in lua modules based on the
|
||||
event_loader lib into the savegame. Regular freeplay games can now be
|
||||
used with Clusterio and will be compatible without having to convert them
|
||||
to Hotpatch.
|
||||
- Daemonized slaves. Slaves now have the ability to run multiple Factorio
|
||||
instances, starting and stopping them individually. To manage this the
|
||||
local command line interface has been replaced with a remote interface on
|
||||
the master server that can be accessed through the clusterioctl cli tool.
|
||||
- Rewritten the communication between slaves and the master from scratch. The
|
||||
new system is based on a WebSocket connection between the slaves and the
|
||||
master server and provides efficient validated bi-directonal communication.
|
||||
- Rewritten the plugin system from scratch. Plugins now inherit from
|
||||
a base class and use the same WebSocket connection Clusterio uses to
|
||||
communicate.
|
||||
- New configuration system with support for initializing configs when needed,
|
||||
modifying config entries using built-in commands, and updating instance
|
||||
configuration remotely.
|
||||
- Packaged and uploaded to npm again, greatly simplifying distribution and
|
||||
installation of both Clusterio and plugins.
|
||||
|
||||
### Features
|
||||
|
||||
- Added export of pollution statitics.
|
||||
- Reconnection logic that esures no data is dropped talking to the master
|
||||
server provided the session can be resumed.
|
||||
- Added support for having multiple Factorio installs and selecting which
|
||||
version to use on a per instance basis.
|
||||
- Added config to specify which path the master interface is accessed under,
|
||||
allowing it to be proxied behind web-server.
|
||||
- Added WebSocket usage statistics.
|
||||
- Added commands sent statistic.
|
||||
- Added Factorio server CPU and memory usage statistic.
|
||||
- Added Factorio autosave size statistic.
|
||||
- Added support for extracting locale and item icons from Factorio and mods.
|
||||
- Added integrated user management for controlling access and storing per
|
||||
player data.
|
||||
- Added synchronization of in-game adminlist, banlist and whitelist to
|
||||
Clusterio core. Previously this was handled by the playerManager plugin.
|
||||
|
||||
### Changes
|
||||
|
||||
- Fixed sslCert and sslPrivKey entries being ignored in the config.
|
||||
- Changed ssl key creation to be done at startup instead of on npm install.
|
||||
- Added error handling during master startup.
|
||||
- Factorio game and rcon port now defaults to a random port above 49151.
|
||||
- Removed unimplemented mods update command.
|
||||
- Fixed rcon password being generated with Math.random().
|
||||
- Added plugins directory to the views path. This makes it possible for
|
||||
plugins to render their own ejs views or pages in their own folders by
|
||||
using paths of the format "pluginName/path/to/page-or-view".
|
||||
- Replaced the per instance copy of the shared Factorio mods with
|
||||
symlinks. On Windows hard links are used instead due to the
|
||||
privileges requirements of symlinks.
|
||||
- Changed the per instance scenario folder to be linked instead of
|
||||
copied on instance creation.
|
||||
- Instance id is no longer derived from the rcon password, instead it's
|
||||
generated upon instance creation and stored in the instance config.
|
||||
- The game port, RCON port and RCON password instance config entries are are
|
||||
now null by default, indicating that a random one will be generated every
|
||||
time the instance is started.
|
||||
- Removed the broken client download command as this would download
|
||||
pre-releases.
|
||||
- Moved the item database and HTTP definitions for /api/place, /api/remove,
|
||||
/api/inventory, /api/inventoryAsObject and the web interface view for the
|
||||
storage page into the clusterioMod plugin. If you disable this plugin
|
||||
then these things will not be available.
|
||||
- Removed undocumented --port and --rcon-port arguments to client.
|
||||
- Removed undocumented FACTORIOPORT and RCONPORT enviroment variable
|
||||
handling in client.
|
||||
- Removed redundant call to /api/slaves in globalChat plugin.
|
||||
- Removed broken serverManager plugin.
|
||||
- Removed factorio_version from config. The version installed is auto
|
||||
detected and used instead.
|
||||
- Removed the playerManager specific command CLI tools/delete_player.js.
|
||||
- Creating an instance, assigning it to a slave, creating a save for an
|
||||
instance and starting an instance is now four separate commands.
|
||||
- Removed oddball limits to slaves.json size.
|
||||
- Moved slave specific and instance specific configuration into their own
|
||||
configuration files.
|
||||
- Removed unused binary option from plugin config.
|
||||
- Removed info and shout command from globalChat plugin
|
||||
- Removed mirrorAllChat and enableCrossServerShout configuration options for
|
||||
globalChat plugin.
|
||||
- Removed UPSdisplay plugin. UPS statistics is exported by the statistics
|
||||
exporter plugin.
|
||||
- Master server now defaults to hosting on https on port 8443.
|
||||
- Renamed client to slave.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Removed lib/authenticate. Breaks playerManager.
|
||||
- The masterIP and masterPort config entries has been merged into masterURL.
|
||||
Breaks discordChat, playerManager, serverSelect, trainTeleports and
|
||||
clusterioModel.
|
||||
- Instance names can no longer be invalid Windows file names.
|
||||
- Removed config management from the command line and the server manager.
|
||||
- Moved ejs templates into views folder and changed their extension to
|
||||
.ejs. Breaks playerManager.
|
||||
- Mods are no longer copied from the per instance instanceMods directory.
|
||||
If you need per instance mods you can now place them directly in the mods
|
||||
directory inside the instance folder.
|
||||
- Hotpatch scenarios and code loading is no longer compatible with Clusterio.
|
||||
Breaks playerManager, serverSelect, and tranTeleports.
|
||||
- Removed getLua and getCommand from lib/clusterTools. If you need to run
|
||||
more than the most trivial of code in commands use the save patcher and
|
||||
add in a remote interface.
|
||||
- Removed mod uploading and distributing from the HTTP interface. Breaks
|
||||
the old unmaintained and no longer needed factorioClusterioClient.
|
||||
- Removed the remoteCommands plugin and the old runCommand interface. Breaks
|
||||
playerManager and external tools sending commands.
|
||||
- Removed broken serverManager plugin.
|
||||
- Removed fields info, time, rconPort, rconPassword, serverPort, unique,
|
||||
mods, instanceName, playerCount, mac, and meta from the slaves in the
|
||||
slave database.
|
||||
- Removed getInstanceName and lib/clusterTools. Breaks playerManager,
|
||||
and discordChat.
|
||||
- Removed the /api/rconPasswords, and /api/slaves endpoints. Breaks web
|
||||
interface, trainTeleports, and discordChat.
|
||||
- Removed the /api/getSlaveMeta and /api/editSlaveMeta endpoints. Breaks
|
||||
researchSync, and UPSdisplay.
|
||||
- Removed the hello event from the socket connection handshake. Breaks
|
||||
playerManager, trainTeleports, serverSelect, and discordChat
|
||||
- Changed the format of database/slaves.json.
|
||||
- Removed the output file subscription system. Breaks inventoryImports,
|
||||
playerManager, trainTeleports, serverSelect and researchSync.
|
||||
- Removed the factorioOutput hook from instance plugins. The onOutput
|
||||
hook provides parsed output instead.
|
||||
- Removed the onLoadFinish hook from master plugins.
|
||||
- Moved plugins from the sharedPlugins directory to plugins directory.
|
||||
- Implemented a new plugin system that replaces the old. Breaks all plugins.
|
||||
- Removed express metric http_request_duration_milliseconds metric.
|
||||
- Removed socket.io metrics socket_io_connected, socket_io_connect_total,
|
||||
socket_io_disconnect_total, socket_io_events_received_total,
|
||||
socket_io_events_sent_total, socket_io_recieve_bytes, and
|
||||
socket_io_transmit_bytes,
|
||||
- Removed clusterio_connected_instaces_gauge and added
|
||||
clusterio_master_connected_clients_count in its place.
|
||||
- Renamed clusterio_player_count_gauge to
|
||||
clusterio_statistics_exporter_instance_player_count
|
||||
- Removed clusterio_UPS_gauge and added
|
||||
clusterio_statistics_exporter_instance_game_ticks_total in its place.
|
||||
- Renamed clusterio_endpoint_hit_gauge to clusterio_http_enpoint_hits_total
|
||||
- Renamed clusterio_statistics_gauge to
|
||||
clusterio_statistics_exporter_instance_force_flows
|
||||
- Renamed clusterio_nn_dole_gauge to clusterio_subspace_storage_nn_dole_gauge.
|
||||
- Renamed clusterio_dole_factor_gauge to
|
||||
clusterio_subspace_storage_dole_factor_gauge.
|
||||
- Renamed clusterio_import_gauge to clusterio_subspace_storage_import_total.
|
||||
- Renamed clusterio_export_gauge to clusterio_subspace_storage_export_total.
|
||||
- Renamed clusterio_master_inventory_gauge to
|
||||
clusterio_subspace_storage_master_inventory.
|
||||
- Removed clusterioMod plugin specific config options logItemTransfers,
|
||||
disableFairItemDistribution useNeuralNetDoleDivider, autosaveInterval, and
|
||||
disableImportsOfEverythingExceptElectricity from the master config.
|
||||
- Removed msBetweenCommands config option. The RCON is instead limited to 5
|
||||
concurrent commands.
|
||||
- Removed allowRemoteCommandExecution config option. Remote commands are
|
||||
always allowed with the move to master managing slaves/instances.
|
||||
- Removed `--databaseDirectory`, `--masterPort`, and `--sslPort` command line
|
||||
arguments from the master server.
|
||||
- Implemented a new config system that replaces the old. Breaks all plugins.
|
||||
- Removed usage of socket.io entirely in favor of a plain WebSocket connection.
|
||||
- Renamed clusterioMod plugin to subspace_storage.
|
||||
- Master no longer creates secret-api-token.txt.
|
||||
|
||||
|
||||
Version 1.2.4
|
||||
-------------
|
||||
|
||||
- Removed broken remote combinator signaling.
|
||||
- Fixed research sync endlessly updating already researched technologies
|
||||
- Removed obsolete item/fluid statistics from clusterioMod
|
||||
- Removed per mod upload logging when config.uploadModsToMaster disabled
|
||||
- Fixed bcrypt failing to install on windows due to the new version not
|
||||
having Windows binaries.
|
||||
|
||||
|
||||
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.
|
||||
@@ -1,41 +0,0 @@
|
||||
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);
|
||||
})();
|
||||
@@ -1,19 +0,0 @@
|
||||
const needle = require("needle")
|
||||
const config = require("./../config.json")
|
||||
const cli = require("cli")
|
||||
|
||||
let options = cli.parse({
|
||||
name: ["n", "Name of the player to delete, case sensitive", "string"],
|
||||
token: [false, "Factorio auth token", "string", config.masterAuthToken]
|
||||
})
|
||||
if(!options.name){
|
||||
cli.getUsage()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
needle.post(config.masterIP+":"+config.masterPort+"/api/playerManager/deletePlayer", {
|
||||
name: options.name,
|
||||
token: options.token
|
||||
}, (err, resp) => {
|
||||
console.log(resp.body)
|
||||
});
|
||||
+6
-4
@@ -1,15 +1,17 @@
|
||||
FROM node:9.11.2
|
||||
# Warning: This is currently broken as a result of
|
||||
# changes done in the 2.0 refactoring.
|
||||
FROM node:12
|
||||
RUN apt-get update && apt install git curl tar -y
|
||||
RUN mkdir factorioClusterio
|
||||
|
||||
RUN git clone -b master https://github.com/Danielv123/factorioClusterio.git && cd factorioClusterio && npm install --only=production
|
||||
RUN git clone -b master https://github.com/clusterio/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
|
||||
RUN cp config.json.dist config.json
|
||||
|
||||
RUN node client.js download
|
||||
RUN node client manage shared mods add clusterio
|
||||
|
||||
LABEL maintainer "Sir3lit@gmail.com"
|
||||
|
||||
@@ -18,4 +20,4 @@ VOLUME /factorioClusterio/instances
|
||||
VOLUME /factorioClusterio/sharedMods
|
||||
VOLUME /factorioClusterio/sharedPlugins
|
||||
|
||||
CMD RCONPORT="$RCONPORT" FACTORIOPORT="$FACTORIOPORT" MODE="$MODE" node $MODE\.js start $INSTANCE
|
||||
CMD MODE="$MODE" node $MODE start $INSTANCE
|
||||
@@ -19,8 +19,3 @@ 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/
|
||||
|
||||
@@ -8,26 +8,38 @@
|
||||
|
||||
Discord for development/support/play: https://discord.gg/5XuDkje
|
||||
|
||||
## Important notice
|
||||
|
||||
This is the development branch for factorioClusterio 2.0 which is currently undergoing heavy
|
||||
restructuring and refactoring. Expect plugins and existing installations to frequently break when
|
||||
using this branch. If you don't want to be an alpha tester for 2.0 please use the stable
|
||||
[1.2.x branch][1.2.x] or
|
||||
[latest stable release](https://github.com/clusterio/factorioClusterio/releases/latest).
|
||||
|
||||
Installation instructions below are for the unstable master branch. Go to the page for
|
||||
the [1.2.x branch][1.2.x] for instructions on how to install the stable version.
|
||||
|
||||
[1.2.x]: https://github.com/clusterio/factorioClusterio/tree/1.2.x
|
||||
|
||||
### Ways to support me/the project:
|
||||
|
||||
* Contribute with code/documentation. Always nice to move the project forward
|
||||
* Contribute with code/documentation. See [Contributing](docs/contributing.md) for how to make pull
|
||||
requests. 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
|
||||
|
||||
* [Introduction & methodology](#introduction)
|
||||
|
||||
* [Ubuntu setup](#ubuntu-setup)
|
||||
|
||||
* [Windows setup](#windows-setup)
|
||||
|
||||
* [Optional plugins](#Plugins)
|
||||
|
||||
* [Installing Plugins](#installing-plugins)
|
||||
* [Running Clusterio](#running-clusterio)
|
||||
* [Master Server](#master-server)
|
||||
* [Slaves](#slaves)
|
||||
* [Instances](#instances)
|
||||
* [Common problems](#Common-problems)
|
||||
|
||||
* [Command cheatsheet](#cheatsheet)
|
||||
|
||||
## Introduction
|
||||
|
||||
Features:
|
||||
@@ -84,144 +96,192 @@ Clusterio can also do a few other neat things, such as giving you access to epoc
|
||||
|
||||
## Ubuntu setup
|
||||
|
||||
NodeJS does not support EOL ubuntu releases. Make sure you are on the most recent LTS release or newer.
|
||||
**Warning**: These instructions are for the unstable master version and is not
|
||||
recommended for use, see [the 1.2.x branch][1.2.x] for how to install the stable version.
|
||||
|
||||
Clusterio runs on Node.js v12 and up, v11.13.0+ and v10.16.0+. Node.js itself is not
|
||||
supported on EOL Ubuntu releases so make sure you're on a recent release of Ubuntu.
|
||||
|
||||
Master and all slaves:
|
||||
|
||||
sudo curl -sL https://deb.nodesource.com/setup_9.x | sudo -E bash -
|
||||
sudo apt install -y nodejs python-dev git wget curl tar build-essential
|
||||
sudo npm install pm2 -g
|
||||
git clone https://github.com/Danielv123/factorioClusterio.git
|
||||
cd factorioClusterio
|
||||
curl -o factorio.tar.gz -L https://www.factorio.com/get-download/latest/headless/linux64
|
||||
wget -qO - https://deb.nodesource.com/setup_12.x | sudo -E bash -
|
||||
sudo apt install -y nodejs
|
||||
mkdir clusterio
|
||||
cd clusterio
|
||||
npm init -y
|
||||
npm install @clusterio/master @clusterio/slave @clusterio/ctl
|
||||
wget -O factorio.tar.gz https://www.factorio.com/get-download/latest/headless/linux64
|
||||
tar -xf factorio.tar.gz
|
||||
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.
|
||||
|
||||
Now you need to edit the `config.json` file. If you skip this step nothing will work.
|
||||
Pretty much all the blank fields should be filled in, except on the master where a few can be omitted.
|
||||
|
||||
* You get the `masterAuthToken` from `secret-api-token.txt` in the master install dir after running the master twice.
|
||||
|
||||
* You get your factorio matchmaking token from factorio.com
|
||||
|
||||
* The `masterAuthSecret` should never be touched unless you want to invalidate everyones authentication tokens
|
||||
|
||||
**Master**
|
||||
|
||||
pm2 start master.js --name master
|
||||
|
||||
OR
|
||||
|
||||
node master.js
|
||||
|
||||
**Server Host**
|
||||
|
||||
To download the mod for all its non vanilla features and items, (optional, but very recommended)
|
||||
|
||||
node client.js manage shared mods add clusterio
|
||||
|
||||
To create a new instance (its own save, set of mods and config files)
|
||||
|
||||
node client.js start [instancename]
|
||||
|
||||
To launch an instance with pm2
|
||||
|
||||
pm2 start --name [instancename] client.js -- start [instancename]
|
||||
|
||||
use `nano config.json` to change settings.
|
||||
downloads and installs nodejs, git and clusterio. To specify a version, change "latest" in the link to a version number like 0.14.21.
|
||||
|
||||
**Ubuntu with Docker**
|
||||
|
||||
The Docker support for Clusterio is curently broken. If you need it
|
||||
open an issue about it.
|
||||
|
||||
<!--
|
||||
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
|
||||
sudo docker run --name slave -e MODE=slave -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.
|
||||
-->
|
||||
|
||||
## Windows setup
|
||||
|
||||
Clusterio is built up of multiple parts. Here is a quick guide:
|
||||
|
||||
Master = master.js
|
||||
|
||||
Server host (Slave) = client.js + factorio server
|
||||
|
||||
Game Client = The people connecting to the server
|
||||
**Warning**: These instructions are for the unstable master version and is not
|
||||
recommended for use, see [the 1.2.x branch][1.2.x] for how to install the stable version.
|
||||
|
||||
**Requirements**
|
||||
|
||||
download and install nodeJS 8 or 9 from http://nodejs.org
|
||||
|
||||
download and install git from https://git-scm.com/
|
||||
|
||||
reboot when you are done, then proceed to the next steps. *reboots matter*
|
||||
download and install nodeJS 12 from http://nodejs.org. Clusterio runs on Node.js v12 and
|
||||
up, v11.13.0+ and v10.16.0+.
|
||||
|
||||
**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
|
||||
mkdir clusterio
|
||||
cd clusterio
|
||||
npm init -y
|
||||
npm install @clusterio/master @clusterio/slave @clusterio/ctl
|
||||
|
||||
3. Follow the instructions in the bat file
|
||||
3. Some of the instructions are outdated. If you get stuck somewhere, look at the Ubuntu section.
|
||||
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. Create a new folder named "factorio" in the the factorioClusterio
|
||||
folder.
|
||||
|
||||
**Server Host**
|
||||
2. Download the MS Windows (64-bit zip package) from
|
||||
https://www.factorio.com/download .
|
||||
|
||||
1. download and run https://puu.sh/toFHl/01eebbb333.bat
|
||||
3. Open the zip file and drag the folder called "Factorio_x.y.z" into
|
||||
the factorio folder created in step 1.
|
||||
|
||||
2. Copy config.json.dist to config.json
|
||||
- Via steam installation
|
||||
|
||||
3. Follow the instructions given.
|
||||
1. Create a new folder named "factorio" in the the factorioClusterio
|
||||
folder.
|
||||
|
||||
3.5 Some of the instructions are outdated. If you get stuck somewhere, look at the Ubuntu section.
|
||||
2. Locate the game files by right clicking the game in steam,
|
||||
selecting properties, then Local Files, then Browse local files.
|
||||
|
||||
4. Type `node client.js start [instancename]` to create a new instance.
|
||||
3. Go to the parent folder of the folder that Steam opened and copy
|
||||
the Factorio folder into the factorio folder created in step 1.
|
||||
|
||||
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)
|
||||
## Installing Plugins
|
||||
|
||||
Change `masterPort` to something like `8080` (provided by master server owner)
|
||||
Installing plugins to make them work with Clusterio consists of two
|
||||
steps. First install the package via npm, for example
|
||||
|
||||
Change `masterAuthToken` to the value found in `secret-api-token.txt` on the master server
|
||||
npm install @clusterio/plugin-subspace_storage
|
||||
|
||||
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)
|
||||
Then tell clusterio that this plugin exists by adding it as a plugin.
|
||||
|
||||
**GameClient**
|
||||
npx clusteriomaster plugin add @clusterio/plugin-subspace_storage
|
||||
|
||||
Fancy game client that does the following steps automatically, but is really old so be warned: [clusterioClient](https://github.com/Danielv123/factorioClusterioClient)
|
||||
This adds it to the default `plugin-list.json` file which in the shared
|
||||
folder setup is loaded by master, slave and ctl. If you have slaves or
|
||||
ctl installed on separate computers (or directories) then you need to
|
||||
repeat the plugin install process for all of them. The clusteriomaster,
|
||||
clusterioslave and clusterioctl commands has the plugin sub-command
|
||||
so you do not need to install clusteriomaster to add plugins.
|
||||
|
||||
1. Download the same version of the mod as the slave is running from [the mod portal](https://mods.factorio.com/mods/Danielv123/clusterio) or [github](https://github.com/Danielv123/factorioClusterioMod
|
||||
For development purposes the `plugin add` command supports adding
|
||||
plugins by the absolute path to them, or a relative path that must start
|
||||
with either . or .. (which will then be resolved to an absolute path).
|
||||
|
||||
2. Drop it into ./factorio/mods
|
||||
|
||||
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
|
||||
## Running Clusterio
|
||||
|
||||
After following the installation instructions you can use the following
|
||||
commands to run Clusterio.
|
||||
|
||||
|
||||
### Setting up an admin account
|
||||
|
||||
Before you can manage the cluster you need to bootstrap an admin account
|
||||
for it. Replace `<username>` with your Factorio username (do not make
|
||||
up a new username here).
|
||||
|
||||
npx clusteriomaster bootstrap create-admin <username>
|
||||
npx clusteriomaster bootstrap create-ctl-config <username>
|
||||
|
||||
The first command creates a user account with the given name and
|
||||
promotes it to a cluster admin. The second one sets up a
|
||||
`config-control.json` config for clusterioctl to connect to the master
|
||||
server under the given user account.
|
||||
|
||||
|
||||
### Running the master Server
|
||||
|
||||
It's necessary to run the master server in order for anything to work.
|
||||
Once you've completed the setup run the following command to start it
|
||||
up:
|
||||
|
||||
npx clusteriomaster run
|
||||
|
||||
|
||||
### Slaves
|
||||
|
||||
Slaves connect to the master server and are managed remotely from the
|
||||
master server. In order for slaves to connect to the master server they
|
||||
need a valid authentication token, you can create a slave config with
|
||||
a valid token with the following command.
|
||||
|
||||
npx clusterioctl slave create-config --name Local --generate-token
|
||||
|
||||
This will write a new `config-slave.json` file in the current directory
|
||||
(you can change the location with the `--output` option) with the name,
|
||||
token and url to connect to the master server with. If you are making
|
||||
the config for a remote slave you will need to have set the
|
||||
`master.external_address` option to the URL the master server can be
|
||||
reached on.
|
||||
|
||||
You can list the config of a slave on the slave itself with the `npx
|
||||
clusterioslave config list` command. Use `npx clusterioslave config
|
||||
--help` for more information.
|
||||
|
||||
Once the config is set up run the slave with
|
||||
|
||||
npx clusterioslave run
|
||||
|
||||
|
||||
### Instances
|
||||
|
||||
Instances are created, managed and started from the master server. For
|
||||
now the only interface available is the `clusterioctl` command line tool
|
||||
included in Clusterio. You can run this tool from any slave, or the
|
||||
master server without having to set up a config, if you want to manage
|
||||
the cluster from somewhere else you will need to set the
|
||||
`control.master_url` and `control.master_token` options with the `node
|
||||
clusterioctl control-config set` command:
|
||||
|
||||
The basic operations to start a new instance is the following
|
||||
|
||||
npx clusterioctl instance create "My instance"
|
||||
npx clusterioctl instance assign "My instance" "Local"
|
||||
npx clusterioctl instance start "My instance"
|
||||
|
||||
The first line creates the instance configuration on the master server.
|
||||
The second assigns the instance to a slave which creates the instance
|
||||
directory and files needed to run the instance on the given slave. The
|
||||
third line starts the instance, which creates a new save if there are no
|
||||
save games present.
|
||||
|
||||
There are many more commands available with clusterioctl. See
|
||||
`npx clusterioctl --help` for a full list of them.
|
||||
|
||||
## 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.
|
||||
@@ -245,36 +305,3 @@ TLDR: the tested fix is:
|
||||
- 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**
|
||||
|
||||
node client.js start [instanceName]
|
||||
|
||||
**Other instance management tools:**
|
||||
```
|
||||
node client.js delete [instanceName]
|
||||
node client.js list
|
||||
```
|
||||
**To update clusterio to the latest version:**
|
||||
|
||||
1. Download the latest zip version of factorio for your platform manually from factorio.com. Place it in the project root folder and call it "factorio" (folder name is specified in config.json)
|
||||
|
||||
2. Grab the latest version of the repo
|
||||
|
||||
```
|
||||
git pull
|
||||
|
||||
npm install --only=production
|
||||
```
|
||||
|
||||
3. Download the latest version of the factorioClusterioMod from its github repo
|
||||
```
|
||||
node client.js manage shared mods add clusterio
|
||||
```
|
||||
Binary file not shown.
@@ -1,796 +0,0 @@
|
||||
const fs = require('fs-extra');
|
||||
const Tail = require('tail').Tail;
|
||||
const https = require('follow-redirects').https;
|
||||
const needle = require("needle");
|
||||
const child_process = require('child_process');
|
||||
const path = require('path');
|
||||
const syncRequest = require('sync-request');
|
||||
const request = require("request");
|
||||
const ncp = require('ncp').ncp;
|
||||
const Rcon = require('rcon-client-fork').Rcon;
|
||||
const deepmerge = require("deepmerge");
|
||||
const getMac = require('getmac').getMac;
|
||||
const rmdirSync = require('rmdir-sync');
|
||||
const ioClient = require("socket.io-client");
|
||||
|
||||
// internal libraries
|
||||
const objectOps = require("lib/objectOps.js");
|
||||
const fileOps = require("lib/fileOps");
|
||||
const stringUtils = require("lib/stringUtils.js");
|
||||
const configManager = require("lib/manager/configManager.js");
|
||||
const hashFile = require('lib/hash').hashFile;
|
||||
|
||||
// argument parsing
|
||||
const args = require('minimist')(process.argv.slice(2));
|
||||
|
||||
// require config.json
|
||||
var config = require(args.config || './config');
|
||||
var global = {};
|
||||
|
||||
if (!fs.existsSync(config.instanceDirectory)) {
|
||||
fs.mkdirSync(config.instanceDirectory);
|
||||
}
|
||||
if (!fs.existsSync("./sharedPlugins/")) {
|
||||
fs.mkdirSync("sharedPlugins");
|
||||
}
|
||||
if (!fs.existsSync("./sharedMods/")) {
|
||||
fs.mkdirSync("sharedMods");
|
||||
}
|
||||
const instance = process.argv[3];
|
||||
const instancedirectory = config.instanceDirectory + '/' + instance;
|
||||
const command = process.argv[2];
|
||||
|
||||
// Set the process title, shows up as the title of the CMD window on windows
|
||||
// and as the process name in ps/top on linux.
|
||||
process.title = "clusterioClient "+instance;
|
||||
|
||||
// add better stack traces on promise rejection
|
||||
process.on('unhandledRejection', r => console.log(r));
|
||||
|
||||
// make sure we have the master access token (can't write to master without it since clusterio 2.0)
|
||||
if(!config.masterAuthToken || typeof config.masterAuthToken !== "string"){
|
||||
console.error("ERROR invalid config!");
|
||||
console.error("Master server now needs an access token for write operations. As clusterio slaves depends \
|
||||
upon this, please add your token to config.json in the field named masterAuthToken. \
|
||||
You can retrieve your auth token from the master in secret-api-token.txt after running it once.");
|
||||
}
|
||||
const needleOptionsWithTokenAuthHeader = {
|
||||
compressed:true,
|
||||
headers: {
|
||||
'x-access-token': config.masterAuthToken
|
||||
},
|
||||
};
|
||||
|
||||
var instanceInfo = {};
|
||||
var commandBuffer=[];
|
||||
// messageInterface Management
|
||||
setInterval(function(){
|
||||
let command=commandBuffer.shift();
|
||||
if(command){
|
||||
messageInterfaceInternal(command[0], command[1], command[2], command[3]);
|
||||
}
|
||||
},config.msBetweenCommands || 50);
|
||||
|
||||
// function to handle sending commands into the game
|
||||
async function messageInterfaceInternal(command, callback, resolve, reject) {
|
||||
// try to save us if you send a buffer instead of string
|
||||
if(typeof command == "object") {
|
||||
command = command.toString('utf8');
|
||||
}
|
||||
|
||||
if(false && process.platform == "linux" && typeof command == "string" && serverprocess) {
|
||||
/*
|
||||
to send to stdin, use:
|
||||
serverprocess.stdin.write("/c command;\n")
|
||||
*/
|
||||
serverprocess.stdin.write(command+"\n");
|
||||
if(typeof callback == "function"){
|
||||
callback();
|
||||
}
|
||||
resolve();
|
||||
} else if(typeof command == "string" && client && client.send && typeof client.send == "function") {
|
||||
try {
|
||||
let str = await client.send(command+"\n");
|
||||
if(typeof callback == "function") callback(str)
|
||||
resolve(str)
|
||||
} catch (err) {
|
||||
console.log("RCON failed, server might be paused or shutting down");
|
||||
// console.log(err);
|
||||
// reject(err);
|
||||
if(typeof callback == "function"){
|
||||
callback();
|
||||
}
|
||||
reject(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
function messageInterface(command, callback) {
|
||||
return new Promise((resolve,reject) => {
|
||||
commandBuffer.push([command,callback, resolve, reject]);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// handle commandline parameters
|
||||
if (!command || command == "help" || command == "--help") {
|
||||
console.error("Usage: ");
|
||||
console.error("node client.js start [instance name]");
|
||||
console.error("node client.js list");
|
||||
console.error("node client.js delete [instance]");
|
||||
console.error("To download the latest version of the Clusterio lua mod, do");
|
||||
console.error("node client.js manage shared mods download clusterio");
|
||||
console.error("To download a clusterio plugin, do");
|
||||
console.error("node client.js manage shared plugins install https://github.com/Danielv123/playerManager");
|
||||
console.error("For more management options, do");
|
||||
console.error("node client.js manage");
|
||||
process.exit(1);
|
||||
} else if (command == "list") {
|
||||
let instanceNames = fileOps.getDirectoriesSync(config.instanceDirectory);
|
||||
instanceNames.unshift("Name:");
|
||||
let longestInstanceName = 0;
|
||||
// determine longest instance name
|
||||
instanceNames.forEach(function(instance){
|
||||
if(instance.length > longestInstanceName) longestInstanceName = instance.length;
|
||||
});
|
||||
let displayLines = [];
|
||||
// push name coloumn to array
|
||||
instanceNames.forEach(function(instance){
|
||||
while(instance.length < longestInstanceName+1){
|
||||
instance += " ";
|
||||
}
|
||||
displayLines.push("| "+ instance + "|");
|
||||
});
|
||||
// create port colloumn
|
||||
let factorioPorts = [];
|
||||
instanceNames.forEach(function(instance){
|
||||
let factorioPort;
|
||||
|
||||
if(instance.includes("Name:")){
|
||||
factorioPort = "Port:"
|
||||
} else {
|
||||
factorioPort = require(path.resolve(config.instanceDirectory, instance, 'config')).factorioPort;
|
||||
}
|
||||
factorioPorts.push(factorioPort);
|
||||
});
|
||||
factorioPorts.forEach((port, index) => {
|
||||
let longestPort = 0;
|
||||
factorioPorts.forEach((port, index) => {
|
||||
if(port.toString().length > longestPort) longestPort = port.toString().length;
|
||||
});
|
||||
while(port.toString().length < longestPort){
|
||||
port += " ";
|
||||
}
|
||||
factorioPorts[index] = port;
|
||||
});
|
||||
instanceNames.forEach(function(instance, index){
|
||||
displayLines[index] += " " + factorioPorts[index] + " |";
|
||||
});
|
||||
|
||||
displayLines.forEach(line => console.log(line));
|
||||
process.exit(0);
|
||||
} else if (command == "manage"){
|
||||
// console.log("Launching mod manager");
|
||||
//const fullUsage = 'node client.js manage [instance, "shared"] ["mods", "config"] ...';
|
||||
function usage(instance, tool, action){
|
||||
if(tool && tool == "mods"){
|
||||
console.log('node client.js manage '+instance+' '+tool+' ["list", "search", "add", "remove", "update"]');
|
||||
} else if(tool && tool == "config") {
|
||||
console.log('node client.js manage '+instance+' '+tool+' ["list", "edit"]');
|
||||
} else if(tool && tool == "plugins") {
|
||||
console.log(`node client.js manage ${instance} ${tool} ["list", "add", "remove"]`);
|
||||
} else {
|
||||
console.log('node client.js manage '+(instance || '[instance, "shared"]') +' '+ (tool || '["mods", "config", "plugins"]') + ' ...');
|
||||
}
|
||||
}
|
||||
const tool = process.argv[4] || "";
|
||||
const action = process.argv[5] || "";
|
||||
if(instance){
|
||||
if(tool == "mods"){
|
||||
(async function(){try{
|
||||
// do require down here to reduce master load time
|
||||
const modManager = require("lib/manager/modManager.js")(config);
|
||||
|
||||
// allow managing mods
|
||||
if(action == "list"){
|
||||
console.log(await modManager.listMods(instance));
|
||||
} else if(action == "search"){
|
||||
console.log(await modManager.findMods(process.argv[6]));
|
||||
} else if(action == "add" || action == "download"){
|
||||
await modManager.addMod(process.argv[6], instance);
|
||||
} else if(action == "remove" || action == "rm" || action == "delete"){
|
||||
await modManager.removeMod(process.argv[6], instance);
|
||||
} else if(action == "update"){
|
||||
await modManager.updateAllMods();
|
||||
} else {
|
||||
usage(instance, tool);
|
||||
}
|
||||
process.exit(0);
|
||||
}catch(e){
|
||||
console.log("Got error from modManager:")
|
||||
console.log(e);
|
||||
}})();
|
||||
} else if(tool == "config"){
|
||||
// allow managing the config
|
||||
if(action == "list" || action == "show" || action == "display"){
|
||||
configManager.displayConfig(instance);
|
||||
} else if(action == "edit"){
|
||||
let newConfigValue = "";
|
||||
process.argv.forEach((arg, i)=>{
|
||||
if(i >= 8){
|
||||
newConfigValue += " "+arg;
|
||||
} else if(i >= 7){
|
||||
newConfigValue += arg;
|
||||
}
|
||||
});
|
||||
configManager.editConfig(instance, process.argv[6], newConfigValue);
|
||||
} else {
|
||||
usage(instance, tool);
|
||||
}
|
||||
} else if(tool == "plugins"){
|
||||
(async function(){try{
|
||||
const pluginManager = require("lib/manager/pluginManager.js")(config);
|
||||
if(action == "list"){
|
||||
await pluginManager.listPlugins();
|
||||
} else if(action == "add" || action == "install" || action == "download"){
|
||||
let status = await pluginManager.addPlugin(process.argv[6]);
|
||||
} else if(action == "remove" || action == "uninstall" || action == "delete"){
|
||||
let status = await pluginManager.removePlugin(process.argv[6]);
|
||||
if(status && status.msg) console.log(status.msg);
|
||||
} else if(action == "enable"){
|
||||
let status = await pluginManager.enablePlugin(process.argv[6]);
|
||||
if(status && status.msg) console.log(status.msg);
|
||||
} else if(action == "disable"){
|
||||
let status = await pluginManager.disablePlugin(process.argv[6]);
|
||||
if(status && status.msg) console.log(status.msg);
|
||||
}
|
||||
process.exit(0);
|
||||
}catch(e){
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}})();
|
||||
} else {
|
||||
usage(instance);
|
||||
}
|
||||
} else {
|
||||
console.log('Usage:');
|
||||
usage(instance);
|
||||
}
|
||||
// process.exit(0);
|
||||
} else if (command == "delete") {
|
||||
if (!process.argv[3]) {
|
||||
console.error("Usage: node client.js delete [instance]");
|
||||
process.exit(1);
|
||||
} else if (typeof process.argv[3] == "string" && fs.existsSync(config.instanceDirectory+"/" + process.argv[3]) && process.argv[3] != "/" && process.argv[3] != "") {
|
||||
fileOps.deleteFolderRecursiveSync(path.resolve(config.instanceDirectory, process.argv[3])); // TODO: Check if this can cause i-craft users to format their server by using wrong paths
|
||||
console.log("Deleted instance " + process.argv[3]);
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.error("Instance not found: " + process.argv[3]);
|
||||
process.exit(0);
|
||||
}
|
||||
} else if (command == "download") {
|
||||
console.log("Downloading mods...");
|
||||
// get JSON data about 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;
|
||||
if(url) {
|
||||
console.log(url);
|
||||
let file = fs.createWriteStream("sharedMods/"+name);
|
||||
https.get(url, function(response) {
|
||||
response.on('end', function () {
|
||||
console.log("Downloaded "+name);
|
||||
process.exit(0);
|
||||
});
|
||||
response.pipe(file);
|
||||
}).end();
|
||||
}
|
||||
} else if (command == "start" && instance === undefined) {
|
||||
console.error("ERROR: No instanceName provided!");
|
||||
console.error("Usage: node client.js start [instanceName]");
|
||||
process.exit(0);
|
||||
} else if (command == "start" && typeof instance == "string" && instance != "/" && !fs.existsSync(instancedirectory)) {
|
||||
// if instance does not exist, create it
|
||||
console.log("Creating instance...");
|
||||
fs.mkdirSync(instancedirectory);
|
||||
fs.mkdirSync(instancedirectory + "/script-output/");
|
||||
fs.mkdirSync(instancedirectory + "/saves/");
|
||||
fs.writeFileSync(instancedirectory + "/script-output/output.txt", "");
|
||||
fs.writeFileSync(instancedirectory + "/script-output/orders.txt", "");
|
||||
fs.writeFileSync(instancedirectory + "/script-output/txbuffer.txt", "");
|
||||
fs.mkdirSync(instancedirectory + "/mods/");
|
||||
fs.mkdirSync(instancedirectory + "/instanceMods/");
|
||||
fs.mkdirSync(instancedirectory + "/scenarios/");
|
||||
ncp("lib/scenarios", path.resolve(instancedirectory, "scenarios"), err => {
|
||||
if (err) console.error(err)
|
||||
});
|
||||
|
||||
// fs.symlinkSync('../../../sharedMods', instancedirectory + "/mods", 'junction') // This is broken because it can only take a file as first argument, not a folder
|
||||
fs.writeFileSync(instancedirectory + `/config.ini`, `[path]\r\n
|
||||
read-data=${ path.resolve(config.factorioDirectory, "data") }\r\n
|
||||
write-data=${ path.resolve(config.instanceDirectory, instance) }\r\n
|
||||
`);
|
||||
|
||||
// this line is probably not needed anymore but Im not gonna remove it
|
||||
fs.copySync('sharedMods', path.join(instancedirectory, "mods"));
|
||||
let instconf = {
|
||||
"factorioPort": args.port || process.env.FACTORIOPORT || Math.floor(Math.random() * 65535),
|
||||
"clientPort": args["rcon-port"] || process.env.RCONPORT || Math.floor(Math.random() * 65535),
|
||||
"__comment_clientPassword": "This is the rcon password. Its also used for making an instanceID. Make sure its unique and not blank.",
|
||||
"clientPassword": args["rcon-password"] || Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 8),
|
||||
"info": {}
|
||||
}
|
||||
console.log("Clusterio | Created instance with settings:")
|
||||
console.log(instconf);
|
||||
|
||||
// create instance config
|
||||
fs.writeFileSync(instancedirectory + "/config.json", JSON.stringify(instconf, null, 4));
|
||||
|
||||
let name = "Clusterio instance: " + instance;
|
||||
if (config.username) {
|
||||
name = config.username + "'s clusterio " + instance;
|
||||
}
|
||||
let serversettings = {
|
||||
"name": name,
|
||||
"description": config.description,
|
||||
"tags": ["clusterio"],
|
||||
"max_players": "20",
|
||||
"visibility": config.visibility,
|
||||
"username": config.username,
|
||||
"token": config.token,
|
||||
"game_password": config.game_password,
|
||||
"verify_user_identity": config.verify_user_identity,
|
||||
"admins": [config.username],
|
||||
"allow_commands": config.allow_commands,
|
||||
"autosave_interval": 10,
|
||||
"autosave_slots": 5,
|
||||
"afk_autokick_interval": 0,
|
||||
"auto_pause": config.auto_pause,
|
||||
};
|
||||
fs.writeFileSync(instancedirectory + "/server-settings.json", JSON.stringify(serversettings, null, 4));
|
||||
console.log("Server settings: "+JSON.stringify(serversettings, null, 4));
|
||||
console.log("Creating save .....");
|
||||
let factorio = child_process.spawn(
|
||||
'./' + config.factorioDirectory + '/bin/x64/factorio', [
|
||||
'-c', instancedirectory + '/config.ini',
|
||||
// '--create', instancedirectory + '/saves/save.zip',
|
||||
'--start-server-load-scenario', 'Hotpatch',
|
||||
'--server-settings', instancedirectory + '/server-settings.json',
|
||||
'--rcon-port', Number(process.env.RCONPORT) || instconf.clientPort,
|
||||
'--rcon-password', instconf.clientPassword,
|
||||
], {
|
||||
'stdio': ['pipe', 'pipe', 'pipe']
|
||||
}
|
||||
);
|
||||
factorio.stdout.on("data", data => {
|
||||
data = data.toString("utf8").replace(/(\r\n\t|\n|\r\t)/gm,"");
|
||||
console.log(data);
|
||||
if(data.includes("Starting RCON interface")){
|
||||
let client = new Rcon({
|
||||
packetResponseTimeout: 200000,
|
||||
maxPending: 5
|
||||
});
|
||||
client.connect({
|
||||
host: 'localhost',
|
||||
port: Number(process.env.RCONPORT) || instconf.clientPort,
|
||||
password: instconf.clientPassword
|
||||
});
|
||||
client.onDidAuthenticate(() => {
|
||||
console.log('Clusterio | RCON Authenticated!');
|
||||
});
|
||||
client.onDidConnect(() => {
|
||||
console.log('Clusterio | RCON Connected, starting save');
|
||||
client.send("/c game.server_save('hotpachSave')");
|
||||
});
|
||||
}
|
||||
if(data.includes("Saving finished")){
|
||||
console.log("Map saved as hotpachSave.zip, exiting...");
|
||||
console.log("Instance created!")
|
||||
process.exit(0);
|
||||
}
|
||||
if(data.includes("Downloading from auth server failed")){
|
||||
console.error("Instance creation failed, unable to establish auth server connection.");
|
||||
console.error("Deleting broken instance...");
|
||||
|
||||
fileOps.deleteFolderRecursiveSync(path.resolve(config.instanceDirectory, process.argv[3]));
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
} else if (command == "start" && typeof instance == "string" && instance != "/" && fs.existsSync(instancedirectory)){(async () => {
|
||||
// Exit if no instance specified (it should be, just a safeguard);
|
||||
if(instancedirectory != config.instanceDirectory+"/undefined"){
|
||||
var instanceconfig = require(path.resolve(instancedirectory,'config'));
|
||||
instanceconfig.unique = stringUtils.hashCode(instanceconfig.clientPassword);
|
||||
if(process.env.FACTORIOPORT){
|
||||
instanceconfig.factorioPort = process.env.FACTORIOPORT;
|
||||
}
|
||||
if(process.env.RCONPORT){
|
||||
instanceconfig.rconPort = process.env.RCONPORT;
|
||||
}
|
||||
} else {
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("Deleting .tmp.zip files");
|
||||
let savefiles = fs.readdirSync(path.join(instancedirectory,"saves"));
|
||||
for(i = 0; i < savefiles.length; i++){
|
||||
if(savefiles[i].substr(savefiles[i].length - 8, 8) == ".tmp.zip") {
|
||||
fs.unlinkSync(path.resolve(instancedirectory, "saves", savefiles[i]));
|
||||
}
|
||||
}
|
||||
console.log("Clusterio | Rotating old logs...");
|
||||
// clean old log file to avoid crash
|
||||
try{
|
||||
let logPath = path.join(instancedirectory,'factorio-current.log');
|
||||
let stat = await fs.stat(logPath);
|
||||
console.log(stat)
|
||||
console.log(stat.isFile())
|
||||
if(stat.isFile()){
|
||||
let logFilename = `factorio-${Math.floor(Date.parse(stat.mtime)/1000)}.log`;
|
||||
await fs.rename(logPath, path.join(instancedirectory, logFilename));
|
||||
console.log(`Log rotated as ${logFilename}`);
|
||||
}
|
||||
}catch(e){}
|
||||
// Math.floor(Date.now()/1000)
|
||||
// (new Date).toGMTString()
|
||||
|
||||
// move mods from ./sharedMods to the instances mod directory
|
||||
try{fs.mkdirSync(path.join(instancedirectory, "instanceMods"));}catch(e){}
|
||||
try{rmdirSync(path.join(instancedirectory, "mods"));}catch(e){}
|
||||
try {
|
||||
// mods directory that will be emptied (deleted) when closing the server to facilitate seperation of instanceMods and sharedMods
|
||||
fs.mkdirSync(path.join(instancedirectory, "mods"));
|
||||
} catch(e){}
|
||||
console.log("Clusterio | Moving shared mods from sharedMods/ to instance/mods...");
|
||||
fs.copySync('sharedMods', path.join(instancedirectory, "mods"));
|
||||
console.log("Clusterio | Moving instance specific mods from instance/instanceMods to instance/mods...");
|
||||
fs.copySync(path.join(instancedirectory, "instanceMods"), path.join(instancedirectory, "mods"));
|
||||
|
||||
// Set paths for factorio so it reads and writes from the correct place even if the instance is imported from somewhere else
|
||||
fs.writeFileSync(instancedirectory + `/config.ini`, `[path]\r\n
|
||||
read-data=${ path.resolve(config.factorioDirectory, "data") }\r\n
|
||||
write-data=${ path.resolve(config.instanceDirectory, instance) }\r\n
|
||||
`);
|
||||
|
||||
// Spawn factorio server
|
||||
//var serverprocess = child_process.exec(commandline);
|
||||
fileOps.getNewestFile(instancedirectory + "/saves/", fs.readdirSync(instancedirectory + "/saves/"),function(err, latestSave) {
|
||||
if(err) {
|
||||
console.error("ERROR!");
|
||||
console.error("Your savefile seems to be missing. This might because you created an instance without having factorio\
|
||||
installed and configured properly. Try installing factorio and adding your savefile to instances/[instancename]/saves/\n");
|
||||
throw err;
|
||||
}
|
||||
// implicit global
|
||||
serverprocess = child_process.spawn(
|
||||
'./' + config.factorioDirectory + '/bin/x64/factorio', [
|
||||
'-c', instancedirectory + '/config.ini',
|
||||
'--start-server', latestSave.file,
|
||||
'--rcon-port', args["rcon-port"] || Number(process.env.RCONPORT) || instanceconfig.clientPort,
|
||||
'--rcon-password', args["rcon-password"] || instanceconfig.clientPassword,
|
||||
'--server-settings', instancedirectory + '/server-settings.json',
|
||||
'--port', args.port || Number(process.env.FACTORIOPORT) || instanceconfig.factorioPort
|
||||
], {
|
||||
'stdio': ['pipe', 'pipe', 'pipe']
|
||||
}
|
||||
);
|
||||
|
||||
serverprocess.on('close', code => {
|
||||
console.log(`child process exited with code ${code}`);
|
||||
process.exit();
|
||||
});
|
||||
serverprocess.stdout.on("data", data => {
|
||||
// log("Stdout: " + data);
|
||||
if(data.toString('utf8').includes("Couldn't parse RCON data: Maximum payload size exceeded")){
|
||||
console.error("ERROR: RCON CONNECTION BROKE DUE TO TOO LARGE PACKET!");
|
||||
console.error("Attempting reconnect...");
|
||||
client.disconnect();
|
||||
client.connect();
|
||||
}
|
||||
// we have to do this to make logs visible on linux and in powershell. Causes log duplication for people with CMD.
|
||||
console.log('Fact: ' + data.toString("utf8").replace("\n", ""));
|
||||
});
|
||||
serverprocess.stderr.on('data', (chunk) => {
|
||||
console.log('ERR: ' + chunk);
|
||||
});
|
||||
|
||||
// connect to the server with rcon
|
||||
if(true || process.platform != "linux"){
|
||||
// IP, port, password
|
||||
client = new Rcon({
|
||||
packetResponseTimeout: 200000, // 200s, should allow for commands up to 1250kB in length
|
||||
maxPending: 5
|
||||
});
|
||||
// check the logfile to see if the RCON interface is running as there is no way to continue without it
|
||||
// we read the log every 2 seconds and stop looping when we start connecting to factorio
|
||||
function checkRcon() {
|
||||
fs.readFile(instancedirectory+"/factorio-current.log", function (err, data) {
|
||||
// if (err) console.error(err);
|
||||
if(data && data.indexOf('Starting RCON interface') > 0){
|
||||
client.connect({
|
||||
host: 'localhost',
|
||||
port: args["rcon-port"] || Number(process.env.RCONPORT) || instanceconfig.clientPort,
|
||||
password: args["rcon-password"] || instanceconfig.clientPassword,
|
||||
});
|
||||
} else {
|
||||
setTimeout(function(){
|
||||
checkRcon();
|
||||
},5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
setTimeout(checkRcon, 5000);
|
||||
|
||||
client.onDidAuthenticate(() => {
|
||||
console.log('Clusterio | RCON Authenticated!');
|
||||
instanceManagement(instanceconfig); // start using rcons
|
||||
});
|
||||
client.onDidConnect(() => {
|
||||
console.log('Clusterio | RCON Connected!');
|
||||
// getID();
|
||||
});
|
||||
client.onDidDisconnect(() => {
|
||||
console.log('Clusterio | RCON Disconnected!');
|
||||
// process.exit(0); // exit because RCON disconnecting is undefined behaviour and we rather just wanna restart now
|
||||
});
|
||||
process.on('SIGINT', function () {
|
||||
console.log("Caught interrupt signal, disconnecting RCON");
|
||||
client.disconnect().then(()=>{
|
||||
console.log("Rcon disconnected, Sending ^C");
|
||||
// We don't actually do this on windows, because ctrl+c in windows CMD sends it to all subprocesses as well. Doing it twice will abort factorios save.
|
||||
if(process.platform == "linux"){
|
||||
serverprocess.kill("SIGINT");
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if(process.platform == "linux"){
|
||||
// don't open an RCON connection and just use stdio instead, does not work on windows.
|
||||
instanceManagement(instanceconfig);
|
||||
process.on('SIGINT', function () {
|
||||
console.log("Caught interrupt signal, sending ^C");
|
||||
serverprocess.kill("SIGINT");
|
||||
});
|
||||
}
|
||||
|
||||
// set some globals
|
||||
confirmedOrders = [];
|
||||
lastSignalCheck = Date.now();
|
||||
});
|
||||
})()} else {
|
||||
console.error("Invalid arguments, quitting.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ensure instancemanagement only ever runs once
|
||||
var _instanceInitialized;
|
||||
async function instanceManagement(instanceconfig) {
|
||||
if (_instanceInitialized) return;
|
||||
_instanceInitialized = true;
|
||||
|
||||
console.log("Started instanceManagement();");
|
||||
|
||||
/* Open websocket connection to master */
|
||||
var socket = ioClient("http://"+config.masterIP+":"+config.masterPort);
|
||||
socket.on("hello", data => {
|
||||
console.log("SOCKET | registering slave!");
|
||||
socket.emit("registerSlave", {
|
||||
instanceID: instanceconfig.unique,
|
||||
});
|
||||
});
|
||||
setInterval(B=> socket.emit("heartbeat"), 10000);
|
||||
|
||||
// load plugins and execute onLoad event
|
||||
const pluginManager = require("lib/manager/pluginManager.js")(config);
|
||||
let pluginsToLoad = await pluginManager.getPlugins();
|
||||
let plugins = [];
|
||||
|
||||
for(let i = 0; i < pluginsToLoad.length; i++){
|
||||
let pluginLoadStarted = Date.now();
|
||||
let combinedConfig = deepmerge(instanceconfig,config,{clone:true});
|
||||
let pluginConfig = pluginsToLoad[i];
|
||||
|
||||
if(!global.subscribedFiles) {
|
||||
global.subscribedFiles = {};
|
||||
}
|
||||
if(pluginConfig.binary == "nodePackage" && pluginConfig.enabled){
|
||||
// require plugin class and execute it
|
||||
let pluginClass = require(path.resolve(pluginConfig.pluginPath, "index.js"));
|
||||
plugins[i] = new pluginClass(combinedConfig, async function(data, callback){
|
||||
if(data && data.toString('utf8')[0] != "/") {
|
||||
console.log("Clusterio | "+ pluginsToLoad[i].name + " | " + data.toString('utf8'));
|
||||
return true;
|
||||
} else if (data && data.toString('utf8')[0] == "/"){
|
||||
return messageInterface(data.toString('utf8'), callback);
|
||||
}
|
||||
}, { // extra functions to pass in object. Should have done it like this from the start, but won't break backwards compat.
|
||||
socket, // socket.io connection to master (and ES6 destructuring, yay)
|
||||
});
|
||||
if(plugins[i].factorioOutput && typeof plugins[i].factorioOutput === "function"){
|
||||
// when factorio logs a line, send it to the plugin. This includes things like autosaves, chat, errors etc
|
||||
serverprocess.stdout.on("data", data => plugins[i].factorioOutput(data.toString()));
|
||||
}
|
||||
if(pluginConfig.scriptOutputFileSubscription && typeof pluginConfig.scriptOutputFileSubscription == "string"){
|
||||
if(global.subscribedFiles[pluginConfig.scriptOutputFileSubscription]) {
|
||||
// please choose a unique file to subscribe to. If you need plugins to share this interface, set up a direct communication
|
||||
// between those plugins instead.
|
||||
throw "FATAL ERROR IN " + pluginConfig.name + " FILE ALREADY SUBSCRIBED " + pluginConfig.scriptOutputFileSubscription;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription)) {
|
||||
// Do something
|
||||
fs.writeFileSync(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription, "");
|
||||
}
|
||||
global.subscribedFiles[pluginConfig.scriptOutputFileSubscription] = true;
|
||||
console.log("Clusterio | Registered file subscription on "+instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription);
|
||||
|
||||
|
||||
if(!pluginConfig.fileReadDelay || pluginConfig.fileReadDelay == 0) {
|
||||
// only wipe the file on restart for now, should most likely be rotated during runtime too
|
||||
fs.writeFileSync(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription, "");
|
||||
let tail = new Tail(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription);
|
||||
tail.on("line", function (data) {
|
||||
plugins[i].scriptOutput(data);
|
||||
});
|
||||
} else {
|
||||
fs.watch(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription, fileChangeHandler);
|
||||
// run once in case a plugin wrote out information before the plugin loaded fully
|
||||
// delay, so the socket got enough time to connect
|
||||
setTimeout(() => {
|
||||
fileChangeHandler(false, pluginConfig.scriptOutputFileSubscription);
|
||||
}, 500);
|
||||
|
||||
// send file contents to plugin for processing
|
||||
function fileChangeHandler(eventType, filename) {
|
||||
if (filename != null) {
|
||||
setTimeout(
|
||||
() => {
|
||||
// get array of lines in file
|
||||
let stuff = fs.readFileSync(instancedirectory + "/script-output/" + filename, "utf8").split("\n");
|
||||
|
||||
// if you found anything, reset the file
|
||||
if (stuff[0]) {
|
||||
fs.writeFileSync(instancedirectory + "/script-output/" + filename, "");
|
||||
}
|
||||
for (let o = 0; o < stuff.length; o++) {
|
||||
if (stuff[o] && !stuff[o].includes('\u0000\u0000')) {
|
||||
try {
|
||||
plugins[i].scriptOutput(stuff[o]);
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
pluginConfig.fileReadDelay || 0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(`Clusterio | Loaded plugin ${pluginsToLoad[i].name} in ${Date.now() - pluginLoadStarted}ms`);
|
||||
} else {
|
||||
// this plugin doesn't have a client portion. Maybe it runs on the master only?
|
||||
}
|
||||
}
|
||||
|
||||
// world IDs ------------------------------------------------------------------
|
||||
hashMods(instance, function(modHashes){
|
||||
setInterval(getID, 10000);
|
||||
getID();
|
||||
function getID() {
|
||||
messageInterface("/silent-command rcon.print(#game.connected_players)", function(playerCount) {
|
||||
var payload = {
|
||||
time: Date.now(),
|
||||
rconPort: instanceconfig.clientPort,
|
||||
rconPassword: instanceconfig.clientPassword,
|
||||
serverPort: instanceconfig.factorioPort,
|
||||
unique: instanceconfig.unique,
|
||||
publicIP: config.publicIP, // IP of the server should be global for all instances, so we pull that straight from the config
|
||||
mods:modHashes,
|
||||
instanceName: instance,
|
||||
info: instanceconfig.info,
|
||||
};
|
||||
if(playerCount){
|
||||
payload.playerCount = playerCount.replace(/(\r\n\t|\n|\r\t)/gm, "");
|
||||
} else {
|
||||
payload.playerCount = 0;
|
||||
}
|
||||
|
||||
function callback(err, mac) {
|
||||
if (err) {
|
||||
mac = "unknown";
|
||||
console.log("##### getMac crashed, but we don't really give a shit because we are probably closing down #####");
|
||||
}
|
||||
global.mac = mac;
|
||||
payload.mac = mac;
|
||||
// console.log("Registered our presence with master "+config.masterIP+" at " + payload.time);
|
||||
needle.post(config.masterIP + ":" + config.masterPort + '/api/getID', payload, needleOptionsWithTokenAuthHeader, function (err, response, body) {
|
||||
if (err && err.code != "ECONNRESET"){
|
||||
console.error("We got problems, something went wrong when contacting master"+config.masterIP+" at " + payload.time);
|
||||
console.error(err);
|
||||
} else if (response && response.body) {
|
||||
// In the future we might be interested in whether or not we actually manage to send it, but honestly I don't care.
|
||||
if(response.body !== "ok") {
|
||||
console.log("Got no \"ok\" while registering our precense with master "+config.masterIP+" at " + payload.time);
|
||||
console.log(response.body);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if(global.mac){
|
||||
callback(undefined, global.mac);
|
||||
} else {
|
||||
getMac(callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
// Mod uploading and management -----------------------------------------------
|
||||
// get mod names and hashes
|
||||
// string: instance, function: callback
|
||||
|
||||
setTimeout(function(){hashMods(instance, uploadMods)}, 5000);
|
||||
function uploadMods(modHashes) {
|
||||
// [{modName:string,hash:string}, ... ]
|
||||
for(let i=0;i<modHashes.length;i++){
|
||||
let payload = {
|
||||
modName: modHashes[i].modName,
|
||||
hash: modHashes[i].hash,
|
||||
};
|
||||
needle.post(config.masterIP + ":" + config.masterPort + '/api/checkMod', payload, needleOptionsWithTokenAuthHeader, function (err, response, body) {
|
||||
if(err) console.error("Unable to contact master server /api/checkMod! Please check your config.json.");
|
||||
if(response && body && body == "found") {
|
||||
console.log("master has mod "+modHashes[i].modName);
|
||||
} else if (response && body && typeof body == "string") {
|
||||
let mod = response.body;
|
||||
if(config.uploadModsToMaster){
|
||||
console.log("Sending mod: " + mod);
|
||||
// Send mods master says it wants
|
||||
// response.body is a string which is a modName.zip
|
||||
var req = request.post({url: "http://"+config.masterIP + ":" + config.masterPort + '/api/uploadMod',
|
||||
headers: {
|
||||
"x-access-token": config.masterAuthToken,
|
||||
},
|
||||
}, function (err, resp, body) {
|
||||
if (err) {
|
||||
console.error(new Error("Unable to contact master server /api/uploadMod! Please check your config.json."));
|
||||
} else {
|
||||
console.log('URL: ' + body);
|
||||
}
|
||||
});
|
||||
var form = req.form();
|
||||
form.append('file', fs.createReadStream(config.instanceDirectory+"/"+instance+"/mods/"+mod));
|
||||
} else {
|
||||
console.log("Not sending mod: " + mod + " to master because config.uploadModsToMaster is not enabled")
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} // END OF INSTANCE START ---------------------------------------------------------------------
|
||||
|
||||
// string, function
|
||||
// returns [{modName:string,hash:string}, ... ]
|
||||
function hashMods(instanceName, callback) {
|
||||
if(!callback) {
|
||||
throw new Error("ERROR in function hashMods NO CALLBACK");
|
||||
}
|
||||
|
||||
let modsDir = config.instanceDirectory+"/"+instanceName+"/mods/";
|
||||
function hashMod(name) {
|
||||
if (path.extname(name) != ".zip") {
|
||||
// Can't hash unzipped mods, return null that's filtered out later
|
||||
return null;
|
||||
} else {
|
||||
return hashFile(modsDir+name).then(hash => (
|
||||
{modName: name, hash: hash}
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let promises = fs.readdirSync(modsDir).map(hashMod);
|
||||
Promise.all(promises).then(hashes => {
|
||||
// Remove null entries from hashMod
|
||||
callback(hashes.filter(entry => entry !== null));
|
||||
});
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
{
|
||||
"__comment": "Where factorio folder. Should be in the same folder as this config file.",
|
||||
"factorioDirectory": "./factorio",
|
||||
"instanceDirectory": "./instances",
|
||||
"databaseDirectory": "./database",
|
||||
|
||||
"__comment1": "IP players should connect to to join your servers, should be portforwarded and stuff",
|
||||
"publicIP": "localhost",
|
||||
|
||||
"__comment2": "IP the master web server runs at, can be ignored on the master system",
|
||||
"masterIP": "localhost",
|
||||
|
||||
"__commentMasterPort": "HTTP port for the master to listen to",
|
||||
"masterPort": 8080,
|
||||
|
||||
"__commentsslPort": "HTTPS port for the master to listen to. Set to 0 or an empty string to not listen for HTTPS.",
|
||||
"sslPort": 443,
|
||||
"sslCert": "database/certificates/cert.crt",
|
||||
"sslPrivKey": "database/certificates/cert.key",
|
||||
|
||||
"__commentMasterAuthToken": "Authentication token to connect to the master server. Find it in secret-api-token.txt on the master.",
|
||||
"masterAuthToken": "",
|
||||
|
||||
"__commentAuthSecret": "This is a long random string used for generating authentication tokens. Do not share. Regenerates when secret-api-token.txt is deleted.",
|
||||
"masterAuthSecret": "",
|
||||
|
||||
"__comment3": "Your factorio.com username for factorio MP services, also username will be admin on new instances",
|
||||
"username": "",
|
||||
"__comment4": "Token from your factorio.com profile page. Required for MP and mod manager services.",
|
||||
"token": "",
|
||||
"__comment7": "Used for communication with the mod portal, not needed if you manage mods by hand",
|
||||
"factorio_version": "0.16.41",
|
||||
|
||||
"__comment5": "General user settings",
|
||||
"verify_user_identity": true,
|
||||
"auto_pause": false,
|
||||
"allow_commands": "admins-only",
|
||||
"game_password": "",
|
||||
"description": "This server runs Clusterio",
|
||||
"visibility": {
|
||||
"public": true,
|
||||
"lan": true
|
||||
},
|
||||
"__comment6": "Statistic gathering statistics, may affect server performance (master memory usage especially)",
|
||||
"itemStats": {
|
||||
"maxEntries":60,
|
||||
"entriesPerSecond": 0.01666666667
|
||||
},
|
||||
"logItemTransfers": false,
|
||||
"__comment6.5": "When the master is undersupplied, we have 3 different modes to 'fairly' distribute items. First to ask gets all, dole division using weird formula or experimental neural net tries to make stuff as fair as possible. Feel free to play around.",
|
||||
"disableFairItemDistribution": false,
|
||||
"useNeuralNetDoleDivider": true,
|
||||
"uploadModsToMaster": true,
|
||||
"msBetweenCommands": 10,
|
||||
"allowRemoteCommandExecution": true,
|
||||
"enableCrossServerShout": true,
|
||||
"mirrorAllChat": false,
|
||||
"__comment7": "Autosave interval for items in the master storage in milliseconds. Defaults to one minute when set to 0 or not set.",
|
||||
"autosaveInterval": 600000,
|
||||
"disablePrometheusPushgateway": false,
|
||||
"__comment8": "This setting only affects the master. It makes every import for things that arent 'electricity' return a maximum of 0. Effectively disables teleport chests.",
|
||||
"disableImportsOfEverythingExceptElectricity": false
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
digraph "Client Connection Flow" {
|
||||
node [shape=box];
|
||||
graph [newrank=true];
|
||||
|
||||
start [label="Start"];
|
||||
wait_hello [label="Wait for hello"];
|
||||
new_session [label="New session"];
|
||||
wait_ready [label="Wait for ready"];
|
||||
connected [label="Connected"];
|
||||
abort [label="Abort"];
|
||||
lost_connection [label="Connection lost or\nunexpectedly closed\nwhile connecting" style="dotted"];
|
||||
|
||||
|
||||
lost_connection -> start [label="Wait random delay"];
|
||||
start -> wait_hello [label="Connect to /api/socket"];
|
||||
wait_hello -> new_session [label="Receive hello"];
|
||||
new_session -> wait_ready [label="Send register_*"];
|
||||
wait_ready -> connected [label="Receive ready"];
|
||||
wait_ready -> abort [label="Connection closed with 4003"];
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 2.42.3 (0)
|
||||
-->
|
||||
<!-- Title: Client Connection Flow Pages: 1 -->
|
||||
<svg width="371pt" height="496pt"
|
||||
viewBox="0.00 0.00 371.00 496.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 492)">
|
||||
<title>Client Connection Flow</title>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-492 367,-492 367,4 -4,4"/>
|
||||
<!-- start -->
|
||||
<g id="node1" class="node">
|
||||
<title>start</title>
|
||||
<polygon fill="none" stroke="black" points="135,-384 81,-384 81,-348 135,-348 135,-384"/>
|
||||
<text text-anchor="middle" x="108" y="-362.3" font-family="Times,serif" font-size="14.00">Start</text>
|
||||
</g>
|
||||
<!-- wait_hello -->
|
||||
<g id="node2" class="node">
|
||||
<title>wait_hello</title>
|
||||
<polygon fill="none" stroke="black" points="164.5,-297 51.5,-297 51.5,-261 164.5,-261 164.5,-297"/>
|
||||
<text text-anchor="middle" x="108" y="-275.3" font-family="Times,serif" font-size="14.00">Wait for hello</text>
|
||||
</g>
|
||||
<!-- start->wait_hello -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>start->wait_hello</title>
|
||||
<path fill="none" stroke="black" d="M108,-347.8C108,-336.16 108,-320.55 108,-307.24"/>
|
||||
<polygon fill="black" stroke="black" points="111.5,-307.18 108,-297.18 104.5,-307.18 111.5,-307.18"/>
|
||||
<text text-anchor="middle" x="187.5" y="-318.8" font-family="Times,serif" font-size="14.00">Connect to /api/socket</text>
|
||||
</g>
|
||||
<!-- new_session -->
|
||||
<g id="node3" class="node">
|
||||
<title>new_session</title>
|
||||
<polygon fill="none" stroke="black" points="161,-210 55,-210 55,-174 161,-174 161,-210"/>
|
||||
<text text-anchor="middle" x="108" y="-188.3" font-family="Times,serif" font-size="14.00">New session</text>
|
||||
</g>
|
||||
<!-- wait_hello->new_session -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>wait_hello->new_session</title>
|
||||
<path fill="none" stroke="black" d="M108,-260.8C108,-249.16 108,-233.55 108,-220.24"/>
|
||||
<polygon fill="black" stroke="black" points="111.5,-220.18 108,-210.18 104.5,-220.18 111.5,-220.18"/>
|
||||
<text text-anchor="middle" x="155.5" y="-231.8" font-family="Times,serif" font-size="14.00">Receive hello</text>
|
||||
</g>
|
||||
<!-- wait_ready -->
|
||||
<g id="node4" class="node">
|
||||
<title>wait_ready</title>
|
||||
<polygon fill="none" stroke="black" points="167.5,-123 48.5,-123 48.5,-87 167.5,-87 167.5,-123"/>
|
||||
<text text-anchor="middle" x="108" y="-101.3" font-family="Times,serif" font-size="14.00">Wait for ready</text>
|
||||
</g>
|
||||
<!-- new_session->wait_ready -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>new_session->wait_ready</title>
|
||||
<path fill="none" stroke="black" d="M108,-173.8C108,-162.16 108,-146.55 108,-133.24"/>
|
||||
<polygon fill="black" stroke="black" points="111.5,-133.18 108,-123.18 104.5,-133.18 111.5,-133.18"/>
|
||||
<text text-anchor="middle" x="164" y="-144.8" font-family="Times,serif" font-size="14.00">Send register_*</text>
|
||||
</g>
|
||||
<!-- connected -->
|
||||
<g id="node5" class="node">
|
||||
<title>connected</title>
|
||||
<polygon fill="none" stroke="black" points="92,-36 0,-36 0,0 92,0 92,-36"/>
|
||||
<text text-anchor="middle" x="46" y="-14.3" font-family="Times,serif" font-size="14.00">Connected</text>
|
||||
</g>
|
||||
<!-- wait_ready->connected -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>wait_ready->connected</title>
|
||||
<path fill="none" stroke="black" d="M64.78,-86.86C57.71,-82.13 51.29,-76.25 47,-69 43.04,-62.32 41.73,-54.22 41.69,-46.49"/>
|
||||
<polygon fill="black" stroke="black" points="45.19,-46.55 42.31,-36.36 38.21,-46.13 45.19,-46.55"/>
|
||||
<text text-anchor="middle" x="97.5" y="-57.8" font-family="Times,serif" font-size="14.00">Receive ready</text>
|
||||
</g>
|
||||
<!-- abort -->
|
||||
<g id="node6" class="node">
|
||||
<title>abort</title>
|
||||
<polygon fill="none" stroke="black" points="198,-36 142,-36 142,0 198,0 198,-36"/>
|
||||
<text text-anchor="middle" x="170" y="-14.3" font-family="Times,serif" font-size="14.00">Abort</text>
|
||||
</g>
|
||||
<!-- wait_ready->abort -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>wait_ready->abort</title>
|
||||
<path fill="none" stroke="black" d="M131.28,-86.66C137.25,-81.45 143.3,-75.4 148,-69 153.19,-61.93 157.5,-53.45 160.88,-45.5"/>
|
||||
<polygon fill="black" stroke="black" points="164.14,-46.78 164.56,-36.19 157.63,-44.21 164.14,-46.78"/>
|
||||
<text text-anchor="middle" x="259.5" y="-57.8" font-family="Times,serif" font-size="14.00">Connection closed with 4003</text>
|
||||
</g>
|
||||
<!-- lost_connection -->
|
||||
<g id="node7" class="node">
|
||||
<title>lost_connection</title>
|
||||
<polygon fill="none" stroke="black" stroke-dasharray="1,5" points="188.5,-488 27.5,-488 27.5,-435 188.5,-435 188.5,-488"/>
|
||||
<text text-anchor="middle" x="108" y="-472.8" font-family="Times,serif" font-size="14.00">Connection lost or</text>
|
||||
<text text-anchor="middle" x="108" y="-457.8" font-family="Times,serif" font-size="14.00">unexpectedly closed</text>
|
||||
<text text-anchor="middle" x="108" y="-442.8" font-family="Times,serif" font-size="14.00">while connecting</text>
|
||||
</g>
|
||||
<!-- lost_connection->start -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>lost_connection->start</title>
|
||||
<path fill="none" stroke="black" d="M108,-434.95C108,-422.36 108,-407.17 108,-394.35"/>
|
||||
<polygon fill="black" stroke="black" points="111.5,-394.24 108,-384.24 104.5,-394.24 111.5,-394.24"/>
|
||||
<text text-anchor="middle" x="175.5" y="-405.8" font-family="Times,serif" font-size="14.00">Wait random delay</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
Executable
+4
@@ -0,0 +1,4 @@
|
||||
for x in $(dirname $0)/*.dot; do
|
||||
echo $x
|
||||
dot $x -Tsvg -o ${x/.dot/.svg}
|
||||
done
|
||||
@@ -0,0 +1,23 @@
|
||||
digraph "Client Reconnection Flow" {
|
||||
node [shape=box];
|
||||
graph [newrank=true];
|
||||
|
||||
reconnect [label="Reconnect"];
|
||||
wait_hello [label="Wait for hello"];
|
||||
resume_session [label="Resume session"];
|
||||
new_session [label="New session" style="dashed"];
|
||||
wait_continue [label="Wait for continue"];
|
||||
connected [label="Connected"];
|
||||
lost_connection [label="Connection lost or\nunexpectedly closed\nwhile reconnecting" style="dotted"];
|
||||
abort [label="Abort"];
|
||||
|
||||
{ rank="same"; wait_continue; new_session };
|
||||
|
||||
lost_connection -> reconnect [label="Wait random delay"];
|
||||
reconnect -> wait_hello [label="Connect to /api/socket"];
|
||||
wait_hello -> resume_session [label="Receive hello"];
|
||||
resume_session -> wait_continue [label="Send resume"];
|
||||
wait_continue -> abort [label="Connection closed with 4003"];
|
||||
wait_continue -> new_session [label="Receive invalidate"];
|
||||
wait_continue -> connected [label="Receive ready"];
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 2.42.3 (0)
|
||||
-->
|
||||
<!-- Title: Client Reconnection Flow Pages: 1 -->
|
||||
<svg width="441pt" height="496pt"
|
||||
viewBox="0.00 0.00 441.00 496.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 492)">
|
||||
<title>Client Reconnection Flow</title>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-492 437,-492 437,4 -4,4"/>
|
||||
<!-- reconnect -->
|
||||
<g id="node1" class="node">
|
||||
<title>reconnect</title>
|
||||
<polygon fill="none" stroke="black" points="153.5,-384 62.5,-384 62.5,-348 153.5,-348 153.5,-384"/>
|
||||
<text text-anchor="middle" x="108" y="-362.3" font-family="Times,serif" font-size="14.00">Reconnect</text>
|
||||
</g>
|
||||
<!-- wait_hello -->
|
||||
<g id="node2" class="node">
|
||||
<title>wait_hello</title>
|
||||
<polygon fill="none" stroke="black" points="164.5,-297 51.5,-297 51.5,-261 164.5,-261 164.5,-297"/>
|
||||
<text text-anchor="middle" x="108" y="-275.3" font-family="Times,serif" font-size="14.00">Wait for hello</text>
|
||||
</g>
|
||||
<!-- reconnect->wait_hello -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>reconnect->wait_hello</title>
|
||||
<path fill="none" stroke="black" d="M108,-347.8C108,-336.16 108,-320.55 108,-307.24"/>
|
||||
<polygon fill="black" stroke="black" points="111.5,-307.18 108,-297.18 104.5,-307.18 111.5,-307.18"/>
|
||||
<text text-anchor="middle" x="187.5" y="-318.8" font-family="Times,serif" font-size="14.00">Connect to /api/socket</text>
|
||||
</g>
|
||||
<!-- resume_session -->
|
||||
<g id="node3" class="node">
|
||||
<title>resume_session</title>
|
||||
<polygon fill="none" stroke="black" points="173.5,-210 42.5,-210 42.5,-174 173.5,-174 173.5,-210"/>
|
||||
<text text-anchor="middle" x="108" y="-188.3" font-family="Times,serif" font-size="14.00">Resume session</text>
|
||||
</g>
|
||||
<!-- wait_hello->resume_session -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>wait_hello->resume_session</title>
|
||||
<path fill="none" stroke="black" d="M108,-260.8C108,-249.16 108,-233.55 108,-220.24"/>
|
||||
<polygon fill="black" stroke="black" points="111.5,-220.18 108,-210.18 104.5,-220.18 111.5,-220.18"/>
|
||||
<text text-anchor="middle" x="155.5" y="-231.8" font-family="Times,serif" font-size="14.00">Receive hello</text>
|
||||
</g>
|
||||
<!-- wait_continue -->
|
||||
<g id="node5" class="node">
|
||||
<title>wait_continue</title>
|
||||
<polygon fill="none" stroke="black" points="178,-123 38,-123 38,-87 178,-87 178,-123"/>
|
||||
<text text-anchor="middle" x="108" y="-101.3" font-family="Times,serif" font-size="14.00">Wait for continue</text>
|
||||
</g>
|
||||
<!-- resume_session->wait_continue -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>resume_session->wait_continue</title>
|
||||
<path fill="none" stroke="black" d="M108,-173.8C108,-162.16 108,-146.55 108,-133.24"/>
|
||||
<polygon fill="black" stroke="black" points="111.5,-133.18 108,-123.18 104.5,-133.18 111.5,-133.18"/>
|
||||
<text text-anchor="middle" x="155" y="-144.8" font-family="Times,serif" font-size="14.00">Send resume</text>
|
||||
</g>
|
||||
<!-- new_session -->
|
||||
<g id="node4" class="node">
|
||||
<title>new_session</title>
|
||||
<polygon fill="none" stroke="black" stroke-dasharray="5,2" points="433,-123 327,-123 327,-87 433,-87 433,-123"/>
|
||||
<text text-anchor="middle" x="380" y="-101.3" font-family="Times,serif" font-size="14.00">New session</text>
|
||||
</g>
|
||||
<!-- wait_continue->new_session -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>wait_continue->new_session</title>
|
||||
<path fill="none" stroke="black" d="M178.05,-105C220.94,-105 275.39,-105 316.94,-105"/>
|
||||
<polygon fill="black" stroke="black" points="316.94,-108.5 326.94,-105 316.94,-101.5 316.94,-108.5"/>
|
||||
<text text-anchor="middle" x="252.5" y="-111.8" font-family="Times,serif" font-size="14.00">Receive invalidate</text>
|
||||
</g>
|
||||
<!-- connected -->
|
||||
<g id="node6" class="node">
|
||||
<title>connected</title>
|
||||
<polygon fill="none" stroke="black" points="92,-36 0,-36 0,0 92,0 92,-36"/>
|
||||
<text text-anchor="middle" x="46" y="-14.3" font-family="Times,serif" font-size="14.00">Connected</text>
|
||||
</g>
|
||||
<!-- wait_continue->connected -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>wait_continue->connected</title>
|
||||
<path fill="none" stroke="black" d="M61.23,-86.99C53.95,-82.28 47.4,-76.37 43,-69 39.01,-62.33 38.18,-54.22 38.78,-46.49"/>
|
||||
<polygon fill="black" stroke="black" points="42.27,-46.77 40.32,-36.36 35.35,-45.72 42.27,-46.77"/>
|
||||
<text text-anchor="middle" x="93.5" y="-57.8" font-family="Times,serif" font-size="14.00">Receive ready</text>
|
||||
</g>
|
||||
<!-- abort -->
|
||||
<g id="node8" class="node">
|
||||
<title>abort</title>
|
||||
<polygon fill="none" stroke="black" points="198,-36 142,-36 142,0 198,0 198,-36"/>
|
||||
<text text-anchor="middle" x="170" y="-14.3" font-family="Times,serif" font-size="14.00">Abort</text>
|
||||
</g>
|
||||
<!-- wait_continue->abort -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>wait_continue->abort</title>
|
||||
<path fill="none" stroke="black" d="M131.28,-86.66C137.25,-81.45 143.3,-75.4 148,-69 153.19,-61.93 157.5,-53.45 160.88,-45.5"/>
|
||||
<polygon fill="black" stroke="black" points="164.14,-46.78 164.56,-36.19 157.63,-44.21 164.14,-46.78"/>
|
||||
<text text-anchor="middle" x="260.5" y="-57.8" font-family="Times,serif" font-size="14.00">Connection closed with 4003</text>
|
||||
</g>
|
||||
<!-- lost_connection -->
|
||||
<g id="node7" class="node">
|
||||
<title>lost_connection</title>
|
||||
<polygon fill="none" stroke="black" stroke-dasharray="1,5" points="188.5,-488 27.5,-488 27.5,-435 188.5,-435 188.5,-488"/>
|
||||
<text text-anchor="middle" x="108" y="-472.8" font-family="Times,serif" font-size="14.00">Connection lost or</text>
|
||||
<text text-anchor="middle" x="108" y="-457.8" font-family="Times,serif" font-size="14.00">unexpectedly closed</text>
|
||||
<text text-anchor="middle" x="108" y="-442.8" font-family="Times,serif" font-size="14.00">while reconnecting</text>
|
||||
</g>
|
||||
<!-- lost_connection->reconnect -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>lost_connection->reconnect</title>
|
||||
<path fill="none" stroke="black" d="M108,-434.95C108,-422.36 108,-407.17 108,-394.35"/>
|
||||
<polygon fill="black" stroke="black" points="111.5,-394.24 108,-384.24 104.5,-394.24 111.5,-394.24"/>
|
||||
<text text-anchor="middle" x="175.5" y="-405.8" font-family="Times,serif" font-size="14.00">Wait random delay</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.1 KiB |
@@ -0,0 +1,21 @@
|
||||
digraph "Server Connection Flow" {
|
||||
node [shape=box];
|
||||
graph [newrank=true];
|
||||
|
||||
new_connection [label="New connection"];
|
||||
wait_handshake [label="Wait for handshake"];
|
||||
validate_register [label="Validate register"];
|
||||
validate_resume [label="Validate resume"];
|
||||
connected [label="Connected"];
|
||||
close [label="Close connection"];
|
||||
|
||||
new_connection -> wait_handshake [label="Send hello"];
|
||||
wait_handshake -> validate_register [label="Receive register_*"];
|
||||
wait_handshake -> validate_resume [label="Receive resume"];
|
||||
wait_handshake -> close [label="Bad handshake\nTimeout"];
|
||||
validate_register -> close [label="Bad token\nBad register"];
|
||||
validate_register -> connected [label="Valid register:\nSend ready"];
|
||||
validate_resume -> close [label="Bad token\nBad resume"];
|
||||
validate_resume -> wait_handshake [label="Bad session:\nsend invalidate"];
|
||||
validate_resume -> connected [label="Valid resume:\nSend continue"];
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 2.42.3 (0)
|
||||
-->
|
||||
<!-- Title: Server Connection Flow Pages: 1 -->
|
||||
<svg width="603pt" height="335pt"
|
||||
viewBox="0.00 0.00 603.00 335.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 331)">
|
||||
<title>Server Connection Flow</title>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-331 599,-331 599,4 -4,4"/>
|
||||
<!-- new_connection -->
|
||||
<g id="node1" class="node">
|
||||
<title>new_connection</title>
|
||||
<polygon fill="none" stroke="black" points="355.5,-327 224.5,-327 224.5,-291 355.5,-291 355.5,-327"/>
|
||||
<text text-anchor="middle" x="290" y="-305.3" font-family="Times,serif" font-size="14.00">New connection</text>
|
||||
</g>
|
||||
<!-- wait_handshake -->
|
||||
<g id="node2" class="node">
|
||||
<title>wait_handshake</title>
|
||||
<polygon fill="none" stroke="black" points="367.5,-240 212.5,-240 212.5,-204 367.5,-204 367.5,-240"/>
|
||||
<text text-anchor="middle" x="290" y="-218.3" font-family="Times,serif" font-size="14.00">Wait for handshake</text>
|
||||
</g>
|
||||
<!-- new_connection->wait_handshake -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>new_connection->wait_handshake</title>
|
||||
<path fill="none" stroke="black" d="M290,-290.8C290,-279.16 290,-263.55 290,-250.24"/>
|
||||
<polygon fill="black" stroke="black" points="293.5,-250.18 290,-240.18 286.5,-250.18 293.5,-250.18"/>
|
||||
<text text-anchor="middle" x="328" y="-261.8" font-family="Times,serif" font-size="14.00">Send hello</text>
|
||||
</g>
|
||||
<!-- validate_register -->
|
||||
<g id="node3" class="node">
|
||||
<title>validate_register</title>
|
||||
<polygon fill="none" stroke="black" points="136,-138 0,-138 0,-102 136,-102 136,-138"/>
|
||||
<text text-anchor="middle" x="68" y="-116.3" font-family="Times,serif" font-size="14.00">Validate register</text>
|
||||
</g>
|
||||
<!-- wait_handshake->validate_register -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>wait_handshake->validate_register</title>
|
||||
<path fill="none" stroke="black" d="M212.45,-216.73C161.73,-212.32 101.57,-203.5 83,-186 72.82,-176.41 68.91,-161.41 67.62,-148.23"/>
|
||||
<polygon fill="black" stroke="black" points="71.11,-147.99 67.07,-138.2 64.12,-148.37 71.11,-147.99"/>
|
||||
<text text-anchor="middle" x="148.5" y="-167.3" font-family="Times,serif" font-size="14.00">Receive register_*</text>
|
||||
</g>
|
||||
<!-- validate_resume -->
|
||||
<g id="node4" class="node">
|
||||
<title>validate_resume</title>
|
||||
<polygon fill="none" stroke="black" points="356,-138 224,-138 224,-102 356,-102 356,-138"/>
|
||||
<text text-anchor="middle" x="290" y="-116.3" font-family="Times,serif" font-size="14.00">Validate resume</text>
|
||||
</g>
|
||||
<!-- wait_handshake->validate_resume -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>wait_handshake->validate_resume</title>
|
||||
<path fill="none" stroke="black" d="M241.15,-203.81C233.91,-199.14 227.42,-193.3 223,-186 216.09,-174.6 216.09,-167.4 223,-156 225.69,-151.55 229.16,-147.65 233.09,-144.22"/>
|
||||
<polygon fill="black" stroke="black" points="235.23,-146.98 241.15,-138.19 231.04,-141.38 235.23,-146.98"/>
|
||||
<text text-anchor="middle" x="280" y="-167.3" font-family="Times,serif" font-size="14.00">Receive resume</text>
|
||||
</g>
|
||||
<!-- close -->
|
||||
<g id="node6" class="node">
|
||||
<title>close</title>
|
||||
<polygon fill="none" stroke="black" points="450.5,-36 313.5,-36 313.5,0 450.5,0 450.5,-36"/>
|
||||
<text text-anchor="middle" x="382" y="-14.3" font-family="Times,serif" font-size="14.00">Close connection</text>
|
||||
</g>
|
||||
<!-- wait_handshake->close -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>wait_handshake->close</title>
|
||||
<path fill="none" stroke="black" d="M367.54,-213.17C403.99,-207.82 442.06,-199.24 454,-186 493.43,-142.29 497.55,-103.05 465,-54 461.92,-49.36 458.09,-45.33 453.78,-41.84"/>
|
||||
<polygon fill="black" stroke="black" points="455.76,-38.95 445.55,-36.1 451.76,-44.69 455.76,-38.95"/>
|
||||
<text text-anchor="middle" x="540.5" y="-123.8" font-family="Times,serif" font-size="14.00">Bad handshake</text>
|
||||
<text text-anchor="middle" x="540.5" y="-108.8" font-family="Times,serif" font-size="14.00">Timeout</text>
|
||||
</g>
|
||||
<!-- connected -->
|
||||
<g id="node5" class="node">
|
||||
<title>connected</title>
|
||||
<polygon fill="none" stroke="black" points="127,-36 35,-36 35,0 127,0 127,-36"/>
|
||||
<text text-anchor="middle" x="81" y="-14.3" font-family="Times,serif" font-size="14.00">Connected</text>
|
||||
</g>
|
||||
<!-- validate_register->connected -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>validate_register->connected</title>
|
||||
<path fill="none" stroke="black" d="M34.78,-101.9C28.43,-97.02 22.64,-91.06 19,-84 12.9,-72.14 12.28,-65.51 19,-54 21.52,-49.69 24.78,-45.87 28.5,-42.49"/>
|
||||
<polygon fill="black" stroke="black" points="30.9,-45.05 36.66,-36.16 26.61,-39.52 30.9,-45.05"/>
|
||||
<text text-anchor="middle" x="70" y="-72.8" font-family="Times,serif" font-size="14.00">Valid register:</text>
|
||||
<text text-anchor="middle" x="70" y="-57.8" font-family="Times,serif" font-size="14.00">Send ready</text>
|
||||
</g>
|
||||
<!-- validate_register->close -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>validate_register->close</title>
|
||||
<path fill="none" stroke="black" d="M136,-111.34C178.72,-105.47 229.03,-96.29 247,-84 260.03,-75.09 253.43,-63.55 266,-54 277.12,-45.55 290.27,-39.13 303.66,-34.26"/>
|
||||
<polygon fill="black" stroke="black" points="305.06,-37.48 313.42,-30.97 302.83,-30.84 305.06,-37.48"/>
|
||||
<text text-anchor="middle" x="311" y="-72.8" font-family="Times,serif" font-size="14.00">Bad token</text>
|
||||
<text text-anchor="middle" x="311" y="-57.8" font-family="Times,serif" font-size="14.00">Bad register</text>
|
||||
</g>
|
||||
<!-- validate_resume->wait_handshake -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>validate_resume->wait_handshake</title>
|
||||
<path fill="none" stroke="black" d="M321.89,-138.33C327.98,-143.2 333.53,-149.09 337,-156 342.98,-167.92 342.98,-174.08 337,-186 335.05,-189.89 332.44,-193.45 329.44,-196.69"/>
|
||||
<polygon fill="black" stroke="black" points="326.86,-194.32 321.89,-203.67 331.61,-199.46 326.86,-194.32"/>
|
||||
<text text-anchor="middle" x="395.5" y="-174.8" font-family="Times,serif" font-size="14.00">Bad session:</text>
|
||||
<text text-anchor="middle" x="395.5" y="-159.8" font-family="Times,serif" font-size="14.00">send invalidate</text>
|
||||
</g>
|
||||
<!-- validate_resume->connected -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>validate_resume->connected</title>
|
||||
<path fill="none" stroke="black" d="M223.7,-112.81C197.28,-107.96 167.61,-99.33 144,-84 129.91,-74.85 132.49,-66.26 121,-54 117.46,-50.23 113.56,-46.43 109.61,-42.79"/>
|
||||
<polygon fill="black" stroke="black" points="111.86,-40.11 102.07,-36.06 107.2,-45.33 111.86,-40.11"/>
|
||||
<text text-anchor="middle" x="195.5" y="-72.8" font-family="Times,serif" font-size="14.00">Valid resume:</text>
|
||||
<text text-anchor="middle" x="195.5" y="-57.8" font-family="Times,serif" font-size="14.00">Send continue</text>
|
||||
</g>
|
||||
<!-- validate_resume->close -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>validate_resume->close</title>
|
||||
<path fill="none" stroke="black" d="M336.9,-101.98C345.46,-97.2 353.66,-91.27 360,-84 369.17,-73.47 374.56,-58.96 377.7,-46.32"/>
|
||||
<polygon fill="black" stroke="black" points="381.18,-46.78 379.85,-36.27 374.33,-45.32 381.18,-46.78"/>
|
||||
<text text-anchor="middle" x="418" y="-72.8" font-family="Times,serif" font-size="14.00">Bad token</text>
|
||||
<text text-anchor="middle" x="418" y="-57.8" font-family="Times,serif" font-size="14.00">Bad resume</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.3 KiB |
@@ -0,0 +1,59 @@
|
||||
Communication Layer
|
||||
===================
|
||||
|
||||
<sub>This document describes the communication layer in Clusterio and
|
||||
unless you're developing Clusterio it's probably not going to be very
|
||||
useful for you.</sub>
|
||||
|
||||
Communication is facilitated by the lib/link library and is based on
|
||||
three core abstractions: links, connectors and messages. Links
|
||||
represents one side of a two way communication pipe, connectors deal
|
||||
with connecting and establishing communication, and messages are the
|
||||
content sent over the link.
|
||||
|
||||
|
||||
Link
|
||||
----
|
||||
|
||||
The Link class represents one side of a two way communication channel
|
||||
where the actual communication is done by a connector associated with
|
||||
the link. The link also knows what the endpoint types it's connected to
|
||||
are via the source and target attributes.
|
||||
|
||||
The endpoint types currently implemented are master, slave, instance,
|
||||
and control. Although slave and instances run in the same Node.js
|
||||
program they use virtual links to communicate between them in order to
|
||||
simplify the communication architecture.
|
||||
|
||||
Upon creation the link will register with the connector in order to
|
||||
receive and process messages from it. The messages received are
|
||||
validated using the validator registered for the type of the message. A
|
||||
message for which there is no validator for is considered an error.
|
||||
|
||||
|
||||
Connector
|
||||
---------
|
||||
|
||||
The connector used with a link is responsible for establishing and/or
|
||||
maintain the connection that messages are sent over. Reconnect logic is
|
||||
expected to be implemented in the connector, though this has not yet
|
||||
been done.
|
||||
|
||||
There's also the virtual connector which lets two links in the same
|
||||
program be connected to each other without having to establish a
|
||||
network connection.
|
||||
|
||||
|
||||
Message
|
||||
-------
|
||||
|
||||
There's two types of messages currently implemented apart from the
|
||||
Link builtin close message and that's Request and Event. The builtin
|
||||
messages are defined in
|
||||
[packages/lib/link/messages.js](/packages/lib/link/messages.js) and are
|
||||
attached to links via the attachAllMessages function, which is usally
|
||||
called in the Link subclass constructor.
|
||||
|
||||
The messages define which links they are valid on and only attaches
|
||||
handlers for those links. There's also a forwarding mechanism for when
|
||||
a request or event is to be forwarded another hop.
|
||||
@@ -0,0 +1,102 @@
|
||||
Configuration System
|
||||
====================
|
||||
|
||||
<sub>This document describes the implementation and inner working of the
|
||||
configuration system in Clusterio, and unless you're developing
|
||||
Clusterio it's probably not going to be very useful for you.</sub>
|
||||
|
||||
Clusterio uses a group based configuration system to manage settings for
|
||||
the master server, instances, slaves, and the clusterioctl utility.
|
||||
Plugins get their own config group for the master server and instances
|
||||
which they can add their own fields to. The fields are defined early in
|
||||
the startup, and the groups and their fields for disabled plugins will
|
||||
still be created.
|
||||
|
||||
The built-in config groups and their fields are defined in
|
||||
[packages/lib/config/definitions.js](/packages/lib/config/definitions.js)
|
||||
while plugins can add their own config groups to the `MasterConfigGroup`
|
||||
and `InstanceConfigGroup` properties of the `info.js` export using
|
||||
subclasses of `PluginConfigGroup`. The plugin group has the enabled
|
||||
field pre-defined on it, and it's used to enable/disable plugins.
|
||||
|
||||
|
||||
Defing Fields
|
||||
-------------
|
||||
|
||||
Fields are defined using the `define` class method on group classes
|
||||
after the groupName have been set and before the class has been
|
||||
finalized. The `define` method takes an object as argument with the
|
||||
following properties:
|
||||
|
||||
**type**:
|
||||
The type of config value this field will support. The supported
|
||||
values are boolean, string, number, and object, and if an attept is
|
||||
made to set the field to a value that is not of the right type it
|
||||
will be reject unless it's a string that can be converted to the
|
||||
right type.
|
||||
|
||||
**name**:
|
||||
Name of the configuration field, this is the primary way the field
|
||||
is accessed, both in code and on the command line. As such it
|
||||
should follow the lower\_case\_underscore style and not contain any
|
||||
spaces or dots.
|
||||
|
||||
**title** (optional):
|
||||
Text used to identify the config field in user interfaces.
|
||||
|
||||
**description** (optional):
|
||||
Text used to describe the config field in user interfaces.
|
||||
|
||||
**enum** (optional):
|
||||
Array of acceptable values this field can have. Attempts to set a
|
||||
value not in this array will be rejected, which makes it useful for
|
||||
making enumareted list of choices.
|
||||
|
||||
**optional** (optional):
|
||||
True if this field can be set to null. Defaults to false.
|
||||
|
||||
**initial_value** (optional if optional is true):
|
||||
Value this config field should take in a newly initialized config.
|
||||
This can also be an async function returning the value to use.
|
||||
|
||||
If the value for a field stored in on disk somehow ends up being invalid
|
||||
the field will take on the initial\_value instead when the config is
|
||||
loaded.
|
||||
|
||||
|
||||
Accessing Fields
|
||||
----------------
|
||||
|
||||
On the Config instance there's two ways to access fields: through the
|
||||
set and get methods using dot notation. E.g.:
|
||||
|
||||
let value = configInstance.get("group.field");
|
||||
|
||||
Or by getting the group first and then calling set or get on that.
|
||||
E.g.:
|
||||
|
||||
let value = configInstance.group("group").get("field")
|
||||
|
||||
The set method takes an extra argument, which is the value to set the
|
||||
field to. If the value is not valid this will throw an InvalidValue
|
||||
exception, if the group or field doesn't exist it will throw an
|
||||
InvalidField exception.
|
||||
|
||||
|
||||
Config Lifecycle
|
||||
----------------
|
||||
|
||||
Values for all config fields are created when the config is initialized,
|
||||
as well as for any fields with missing values when a config is loaded.
|
||||
Once a field has a value it is never automatically removed, even if the
|
||||
field or group it was part of no longer exists. This ensures that
|
||||
plugin configuration is not lost should the server be started up without
|
||||
the plugins that defined the configuration installed.
|
||||
|
||||
For instance configs, they are kept on the master server and serialized
|
||||
and sent over to the relevant slaves on startup. This makes it possible
|
||||
to edit instance configs when the slave the instance is on is offline.
|
||||
A copy is also stored in the instance directory on the slave, and this
|
||||
config will be deliverd to the master server on slave startup, which
|
||||
enables instances to be copied between clusters as long as their IDs are
|
||||
unique.
|
||||
@@ -0,0 +1,264 @@
|
||||
Contributing to Clusterio
|
||||
=========================
|
||||
|
||||
You are probably reading this because you want to contribute code to
|
||||
Clusterio. Great! We need people like you. Though we have some
|
||||
conventions and workflows that we kindly ask you to follow.
|
||||
|
||||
|
||||
Contents
|
||||
--------
|
||||
|
||||
- [Workflow](#workflow)
|
||||
- [Starting a Feature Branch](#starting-a-feature-branch)
|
||||
- [Setting up the Development Environment](#setting-up-the-development-environment)
|
||||
- [Checking your code for errors](#checking-your-code-for-errors)
|
||||
- [Submitting a Pull Request](#submitting-a-pull-request)
|
||||
- [Editing your Feature Branch](#editing-your-feature-branch)
|
||||
- [Resolving Merge Conflicts](#resolving-merge-conflicts)
|
||||
- [Updating Your GitHub Fork](#updating-your-github-fork)
|
||||
- [Changelog](#changelog)
|
||||
- [Supported Node.js and Factorio Version](#supported-node.js-and-factorio-version)
|
||||
- [Code Style](#code-style)
|
||||
- [Naming Style](#naming-style)
|
||||
- [Indenting](#indenting)
|
||||
- [Line Length](#line-length)
|
||||
|
||||
|
||||
Workflow
|
||||
--------
|
||||
|
||||
While we don't require you to follow this workflow, it makes things much
|
||||
simpler for both you and us if you choose to do so. Especially when it
|
||||
comes to making the commit history reflect what happened and not be a
|
||||
giant ball of mess. GitHub's git workflow is less than ideal for
|
||||
dealing with small feature-branches so you might see things being done
|
||||
here a little diffrent than the usual.
|
||||
|
||||
It is recommended that you use the command line git version for doing
|
||||
the operations described here. GUI wrappers tend to have different
|
||||
ideas of what is what and you might find it difficult to make it do the
|
||||
things described here. For doing the actual commits though, your
|
||||
faviorit git GUI should work just fine.
|
||||
|
||||
If you pressed the fork button on GitHub and used `git clone` _on your
|
||||
own fork_ you can skip to the next paragraph. Otherwise you will have
|
||||
to click the fork button and clone your fork. If you already cloned the
|
||||
main GitHub repository you can change your clone to point at your own
|
||||
fork using the `git remote` command, but it's probably easier to delete
|
||||
your local clone and clone it again.
|
||||
|
||||
To keep your local repository up to to date with the main repository add
|
||||
the main repository as a remote.
|
||||
|
||||
git remote add upstream https://github.com/clusterio/factorioClusterio
|
||||
|
||||
After this running `git fetch upstream` will get the updated branches
|
||||
from the main repository. Do not run `git pull upstream` it will mess
|
||||
thing up!
|
||||
|
||||
|
||||
### Setting up the Development Environment
|
||||
|
||||
The project is organized into several packages that are all managed
|
||||
through Lerna. In order to get the development environment up and
|
||||
running you will need to run the following commands:
|
||||
|
||||
npm install
|
||||
npx lerna bootstrap
|
||||
|
||||
This installs dependencies needed by tests and links the packages up so
|
||||
they work from the git work tree.
|
||||
|
||||
|
||||
### Starting a Feature Branch
|
||||
|
||||
Development is done exclusively in feature branches. This means the
|
||||
first thing to do before starting any development at all is to make a
|
||||
new branch starting from the master or 1.2.x branch of the main
|
||||
repository. To do this check out that branch from the main repository
|
||||
(substitute master for 1.2.x if you're working on that branch.)
|
||||
|
||||
git fetch upstream
|
||||
git checkout upstream/master
|
||||
|
||||
This will give a warning about detached HEAD which means you did it
|
||||
correctly. Now you can make a feature branch based on this with.
|
||||
|
||||
git checkout -b my-feature-branch
|
||||
|
||||
The name of the branch is not important, as long as it's not a name
|
||||
you've used before. But it's nice to have its name be somewhat
|
||||
descriptive of what it implements.
|
||||
|
||||
|
||||
### Checking your code for errors
|
||||
|
||||
There's automated tests and linting setup for the project. Please do
|
||||
add tests for any code you add and run both:
|
||||
|
||||
npm run test
|
||||
npm run lint
|
||||
|
||||
To check that your changes pass the integration tests as well as the
|
||||
ESLint rules set up for the project.
|
||||
|
||||
|
||||
### Submitting a Pull Request
|
||||
|
||||
Once you've commited changes to your feature branch that you want to
|
||||
have included into the main repository push it to you fork with
|
||||
|
||||
git push origin my-feature-branch
|
||||
|
||||
and make a pull request through the GitHub web interface. You can also
|
||||
pass the `--set-upstream` (or `-u`) flag to make `git push` do the same
|
||||
thing for this branch. After your feature branch have been merged you
|
||||
should consider it final. If there are more changes you want to have
|
||||
merged or mistakes you need to fix you must start a new feature branch
|
||||
and submit a new pull request
|
||||
|
||||
|
||||
### Editing your Feature Branch
|
||||
|
||||
Until your feature branch is merged you can edit it as you see fit by
|
||||
using the rebase tool. If your feature branch has been merged however
|
||||
it's final and you will have to start a new feature branch and submit
|
||||
that in a new pull request. The base usage of rebase assuming you are
|
||||
checked out on your feature branch is the following command (substitute
|
||||
master with 1.2.x if your feature branch is based on that)
|
||||
|
||||
git rebase -i upstream/master
|
||||
|
||||
This will open up an editor with the commits of your feature branch and
|
||||
let you reorder, edit, and remove commits as you see fit.
|
||||
|
||||
If the main repository has been updated and you have fetched down those
|
||||
updates with `git fetch upstream` this will also move your feature
|
||||
branch so that it is based on the latest commit from the main
|
||||
repository.
|
||||
|
||||
If you have already pushed your feature branch to your fork a simple
|
||||
`git push origin my-feature-branch` will no longer work as the histories
|
||||
are different, instead you will have to run
|
||||
|
||||
git push origin +my-feature-branch
|
||||
|
||||
The + sign signifies a force push and should be used with care. If you
|
||||
have a pull request that has not yet been merged submitted for this
|
||||
feature branch this will update the pull request too.
|
||||
|
||||
|
||||
### Resolving Merge Conflicts
|
||||
|
||||
The best way to resolve merge conflicts in feature braches is to rebase
|
||||
it on top of the updated upstream. Run the following commands
|
||||
(substitute master with 1.2.x if your feature branch is based on that)
|
||||
|
||||
git fetch upstream
|
||||
git rebase upstream/master
|
||||
|
||||
This will stop on the commit(s) that caused the merge conflict and let
|
||||
you resolve them manually before continuing. Once you're done force
|
||||
push your updated feature branch with
|
||||
|
||||
git push origin +my-feature-branch
|
||||
|
||||
|
||||
### Updating Your GitHub Fork
|
||||
|
||||
No. This is pointless busywork that accomplishes nothing. Fetch the
|
||||
main repository directly as described above and base your feature
|
||||
branches on the refs from it. GitHub has no interface or functionality
|
||||
for keeping the branches in your fork up to date with the branches in
|
||||
the main repository and there is no point in doing this for our feature
|
||||
branch based workflow.
|
||||
|
||||
|
||||
Changelog
|
||||
---------
|
||||
|
||||
There's a [Changelog](../CHANGELOG.md) in the root of the project. If
|
||||
you make changes that are visible to users of this project you should
|
||||
add an entry to the changelog describing what has changed. The format
|
||||
of this log should be self explanatory, please follow it carefully.
|
||||
|
||||
|
||||
Supported Node.js and Factorio Versions
|
||||
---------------------------------------
|
||||
|
||||
Clusterio run on Node.js v12 and up, v11.13.0+ and v10.16.0+, make sure
|
||||
to check that the Node builtin API's and npm packages you use are
|
||||
supported on these versions. The Travis CI tests are runned on whatever
|
||||
version of Node.js v10 is the default, which at the time of writing is
|
||||
v10.18.0.
|
||||
|
||||
For Factorio Clusterio 2.0 aims to support version 0.17.69 and up,
|
||||
including the latest experimental release. It's recommended that you
|
||||
use the lua API reference for 0.17.69, as there's no information on what
|
||||
version Factorio API's were introduced in. The Travis CI tests are
|
||||
runned against latest experimental release.
|
||||
|
||||
|
||||
Code Style
|
||||
----------
|
||||
|
||||
The style of the code is a bit of a mixed bag at the moment. But there
|
||||
are at least a few things that have been agreed upon.
|
||||
|
||||
|
||||
### Naming style
|
||||
|
||||
JavaScript variables in general uses camelCase. The exception is for
|
||||
classes/constructors which use PascalCase and variables destructured
|
||||
from over the wire objects.
|
||||
|
||||
Fields in configs and messages sent over links uses
|
||||
lowercase_underscore.
|
||||
|
||||
JavaScript files are named using lowercase_underscore for files that
|
||||
export multiple items. Files containing and exporting a single class
|
||||
should be named the same as the class in PascalCase.
|
||||
|
||||
Imported modules from the lib package are prefixed with lib to make them
|
||||
easier to distinguish, as the names tend to be very generic. E.g.:
|
||||
|
||||
const libLink = require("lib/link");
|
||||
const libLuaTools = require("lib/lua_tools");
|
||||
|
||||
|
||||
For lua code lowercase_underscore is used for everything.
|
||||
|
||||
|
||||
### Strings
|
||||
|
||||
JavaScript and Lua code should use double quoted strings (`"string"`)
|
||||
for all strings with the only exception that strings that contain double
|
||||
quotes may use single quotes (i.e., `'String with "quotes"'`).
|
||||
JavaScript template literals may also be used where appropriate.
|
||||
|
||||
|
||||
### Indenting
|
||||
|
||||
JavaScript code should be indented with tabs.
|
||||
|
||||
Lua code should be indented with 4 spaces.
|
||||
|
||||
|
||||
### Line Length
|
||||
|
||||
Lines are limited to 120 characters where tabs count as 4 characters.
|
||||
If the argument list for a function or expressino for compound statement
|
||||
gets very long break them out on their own line(s). For example:
|
||||
|
||||
function foo(
|
||||
really, long, argument, list,
|
||||
over, multiple, lines
|
||||
) {
|
||||
if (
|
||||
really && long && expression
|
||||
|| over && multiple && lines
|
||||
) {
|
||||
// code
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,387 @@
|
||||
Developing for Clusterio
|
||||
========================
|
||||
|
||||
This document describes the various different types of content that can
|
||||
be created for Factorio and/or Clusterio and how this is made compatible
|
||||
with Clusterio.
|
||||
|
||||
|
||||
Contents
|
||||
--------
|
||||
|
||||
- [Factorio Mods](#factorio-mods)
|
||||
- [Factorio Scenarios](#factorio-scenarios)
|
||||
- [event_handler interface](#event_handler-interface)
|
||||
- [Communicating with Clusterio](#communicating-with-clusterio)
|
||||
- [Clusterio Modules](#clusterio-modules)
|
||||
- [Clusterio Lua API](#clusterio-lua-api)
|
||||
- [Clusterio Plugins](#clusterio-plugins)
|
||||
|
||||
|
||||
Factorio Mods
|
||||
-------------
|
||||
|
||||
Factorio mods that do not interact with Clusterio should not have any
|
||||
considerations that need to be taken into account for them to work with
|
||||
Clusterio.
|
||||
|
||||
|
||||
Factorio Scenarios
|
||||
------------------
|
||||
|
||||
Clusterio patches saves and because of this scenarios have a few
|
||||
limitations and must explicitly mark themselves as compatible with
|
||||
Clusterio to work. The main limitation is that the
|
||||
[event_handler](https://github.com/wube/factorio-data/blob/master/core/lualib/event_handler.lua)
|
||||
lib must be used, and control.lua cannot contain any code other than
|
||||
calls to event_handler loading the relevant libs of the scenario.
|
||||
This is because the control.lua file will be overwritten by the save
|
||||
patcher. Scenarios also cannot use the event registering functions
|
||||
exposed by `script` such as `script.on_event` or `script.on_nth_tick` as
|
||||
using these will overwrite the handlers registered by the
|
||||
event_handler lib.
|
||||
|
||||
A brief description of the usage of the event_handler library is
|
||||
provided in the [event_handler interface](#event_handler-interface)
|
||||
section.
|
||||
|
||||
Once a scenario is made using the event_handler it should have a
|
||||
control.lua file that looks something like this:
|
||||
|
||||
```lua
|
||||
-- control.lua
|
||||
local event_handler = require("event_handler")
|
||||
event_handler.add_lib(require("module_foo"))
|
||||
event_handler.add_lib(require("module_bar"))
|
||||
```
|
||||
|
||||
To make Clusterio recognize and correctly handle this scenario it
|
||||
needs to include a clusterio.json file with following content:
|
||||
|
||||
```json
|
||||
{
|
||||
"scenario": {
|
||||
"name": "example-scenario",
|
||||
"modules": [
|
||||
"module_foo",
|
||||
"module_bar"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This tells Clusterio that it should load both module_foo and module_bar
|
||||
by passing the result of requiring them to the `add_lib` function when
|
||||
writing a new control.lua file to the save.
|
||||
|
||||
|
||||
### event_handler interface
|
||||
|
||||
<sub>**Note:** At the time of writing the event_handler lib has been
|
||||
a part of Factorio since at least 0.17.4, and was moved from base to
|
||||
core in 0.17.63 but is still not documented anywhere. A brief
|
||||
description is provided here for this reason.</sub>
|
||||
|
||||
The event_handler lib provides a simple interface for registering
|
||||
multiple callbacks to the same events without having to be concerned
|
||||
with callback chaining or overwriting callbacks defined elsewhere. It
|
||||
works by taking over the task of registering the actual callbacks with
|
||||
Factorio, providing its own interface for the rest of the code to use.
|
||||
|
||||
The sole function of interest exported by event_handler is `add_lib`
|
||||
which accepts a table with event handler callbacks definitions that it
|
||||
will register. The following entries are recognized by `add_lib`, all
|
||||
of which are optional:
|
||||
|
||||
- `on_init`:
|
||||
Callback called when the callback of `script.on_init` is invoked.
|
||||
- `on_load`:
|
||||
Callback called when the callback of `script.on_load` is invoked.
|
||||
- `on_configuration_changed`:
|
||||
Callback called when the callback of
|
||||
`script.on_configuration_changed` is invoked.
|
||||
- `events`:
|
||||
Table mapping event ids as defined in `defines.events` to callbacks.
|
||||
For example the table `{ [defines.events.on_player_died] = foo }`
|
||||
will call bar with the usual `event` table as argument every time a
|
||||
player dies.
|
||||
- `on_nth_tick`:
|
||||
Table mapping the nth tick number to a callback. For example the
|
||||
table `{ [30] = bar }` will call bar every 30th tick.
|
||||
- `add_remote_interface`:
|
||||
Callback called before on_init/on_load callbacks<sup>[1]</sup>. It
|
||||
has no special meaning and receives no arguments but should be used
|
||||
for registering remote interfaces.
|
||||
- `add_commands`:
|
||||
Callback called before on_init/on_load callbacks<sup>[1]</sup>. It
|
||||
has no special meaning and receives no arguments but should be used
|
||||
for registering commands.
|
||||
|
||||
<sub>1: Before 0.17.69 these callbacks were called after
|
||||
on_init/on_load.</sub>
|
||||
|
||||
The usual way to use `add_lib` is to define the table of events to
|
||||
register in a separate file and return it, then load it in control.lua
|
||||
via `require` before passing it to `add_lib`. For example a module
|
||||
could be defined as
|
||||
|
||||
```lua
|
||||
-- example.lua
|
||||
local function my_handler(event)
|
||||
local name = game.player[event.player_index].name
|
||||
game.print("It appears that " .. name .. " has died")
|
||||
end
|
||||
|
||||
return {
|
||||
on_init = function() game.print("example_lib init") end,
|
||||
events = {
|
||||
[defines.events.on_player_died] = my_handler,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
And then in control.lua the following used to load it with `add_lib`:
|
||||
|
||||
```lua
|
||||
-- control.lua
|
||||
local event_handler = require("event_handler")
|
||||
event_handler.add_lib(require("example"))
|
||||
```
|
||||
|
||||
Because the event_handler lib registers events itself you may not use
|
||||
`script.on_load` or `script.on_init` at all in your code and any usage
|
||||
of `script.on_configuration_changed`, `script.on_nth_tick` and/or
|
||||
`script.on_event` will cause the corresponding events registered with
|
||||
event_handler to break and should therefore not be used.
|
||||
|
||||
There's also the `add_libraries` function exported by event_handler,
|
||||
which accepts a table and calls `add_lib` for each value in the table.
|
||||
|
||||
|
||||
Communicating with Clusterio
|
||||
----------------------------
|
||||
|
||||
There's a poorly to virtuarly undocumented HTTP interface that can be
|
||||
used to communicate with Clusterio to do things like get the status of
|
||||
instances, grab items from the store, run commands on instances, etc,
|
||||
etc. For now you can read the source code, and maybe ask on the Discord
|
||||
for some pointers if you get stuck.
|
||||
|
||||
|
||||
Clusterio Modules
|
||||
-----------------
|
||||
|
||||
Modules are primarily used by plugins to inject code into Factorio games
|
||||
with the save patcher, though it's also possible to make stand alone
|
||||
modules that are loaded into the game if you don't need the capabilites
|
||||
of the plugin system. The save patcher puts modules into the `modules`
|
||||
folder of the Factorio save and adds code to `control.lua` to load the
|
||||
module according to the `load` and `require` options to the module.json
|
||||
file.
|
||||
|
||||
Stand alone modules are placed into the modules folder of Clusterio,
|
||||
plugin modules are located in the module folder of the plugin. In
|
||||
either case a `module.json` file is required and has the following
|
||||
structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my_module",
|
||||
"version": "1.2.0",
|
||||
"dependencies": {
|
||||
"clusterio": "*",
|
||||
"foo": ">=0.4.2"
|
||||
},
|
||||
"require": ["bar.lua"],
|
||||
"load": ["foo.lua"]
|
||||
}
|
||||
```
|
||||
|
||||
The following entries are supported in the module.json file:
|
||||
|
||||
- `name`:
|
||||
Name of the module, must match the folder the module is located in
|
||||
for stand alone modules or the name of the plugin the module is
|
||||
located in.
|
||||
- `version`:
|
||||
Version of the module. Must be compatible with [Semantic Versioning
|
||||
2.0.0](https://semver.org/). In plugin modules this defaults to the
|
||||
version of the plugin otherwise it's required.
|
||||
- `dependencies`:
|
||||
Optional mapping of modules this module depends on and their
|
||||
version. In the example `>=0.4.2` means that the foo module must be
|
||||
at least version 0.4.2. See the [Ranges
|
||||
syntax](https://www.npmjs.com/package/semver#ranges) of the
|
||||
node-semver package for a full description of what operators are
|
||||
supported. The events for dependencies is invoked before the events
|
||||
of the dependent, and starting an instance will fail if the
|
||||
dependencies cannot be satisfied. If not specified it will default
|
||||
to depending on `clusterio`. Make sure to add `clusterio` to your
|
||||
dependencies if you add other dependencies and depend on the Lua API.
|
||||
- `require`:
|
||||
Lua files to require in `control.lua`. This should only really be
|
||||
needed if you want to make a global function available for use in
|
||||
Lua commands. The paths are relative to the module's own folder and
|
||||
should be specified using forward slashes as directory sepparators
|
||||
if it's located in a sub directory in the module.
|
||||
- `load`:
|
||||
Lua files to load with the `event_handler` lib, the result of
|
||||
requiring the file will be passed to the `add_lib` function of the
|
||||
`event_handler` lib in `control.lua`. See the section on the
|
||||
[event_handler interface](#event_handler-interface) for a detailed
|
||||
description on how the `event_handler` lib works. The paths are
|
||||
relative to the module's own folder and should be specified using
|
||||
forward slashes as directory sepparators if it's located in a sub
|
||||
directory in the module.
|
||||
|
||||
It's recommended to use the new style of defining modules in Lua, as
|
||||
well as avoid the use of global variables as much as possible. This
|
||||
means always declearing your top level variables and functions local and
|
||||
exporting only the things you need in other files. You can require
|
||||
other files in your module by prefixing your require paths with
|
||||
`modules/your_module_name/`. It's also possible to require files from
|
||||
other modules this way too.
|
||||
|
||||
The global variables and data stored in the global table is shared by
|
||||
all Clusterio modules as well as the scenario, so it's important that
|
||||
you use unique names. It's recommended to prefix the global variables
|
||||
and data in the global table that your module has with the name of your
|
||||
module.
|
||||
|
||||
Because modules can be patched into an existing game you cannot rely on
|
||||
the `on_init` callback to be called in Clusterio Modules. Nor can you
|
||||
rely on the `on_configuration_changed` callback, as this is not called
|
||||
when level code changes. The Clusterio Lua API provides the custom
|
||||
`on_server_startup` event that can be used as a substitute, see the next
|
||||
section.
|
||||
|
||||
|
||||
Clusterio Lua API
|
||||
-----------------
|
||||
|
||||
Clusterio provides a save patched module as well as a regular Factorio
|
||||
mod for interfacing with Clusterio. This includes tools sending data to
|
||||
plugins, script events to listen for, and an item serialization module.
|
||||
|
||||
|
||||
### clusterio_api library
|
||||
|
||||
**require path**
|
||||
From a module
|
||||
`local clusterio_api = require("modules/clusterio/api")`
|
||||
From a mod
|
||||
`local clusterio_api = require("__clusterio_lib__/api")`
|
||||
|
||||
Provides the main interface to Clusterio from within the game. When
|
||||
using the mod version you must call `init` before it'll function
|
||||
properly.
|
||||
|
||||
#### init
|
||||
|
||||
<sub>Mod version only</sub>
|
||||
|
||||
Initialize the API. This should be called from both the `on_init` and
|
||||
`on_load` events in your mod, and before this is called certain features
|
||||
will not be available, most notably the events table.
|
||||
|
||||
#### clusterio_api.events
|
||||
|
||||
Table of events raised by clusterio. If the game is started outside of
|
||||
Clusterio then none of these events will be raised.
|
||||
|
||||
In the mod version this table does not exist before the `init` function
|
||||
is called.
|
||||
|
||||
##### on_instance_updated
|
||||
|
||||
Raised after the name and id of an instance has been updated. This may
|
||||
occur even if the id and name didn't change.
|
||||
|
||||
Event data:
|
||||
- `instance_id`: the id of the instance.
|
||||
- `instance_name`: the name of the instance.
|
||||
|
||||
##### on_server_startup
|
||||
|
||||
Raised when Clusterio is starting up the server. It can be used to
|
||||
initialize clusterio related features in a mod, and as a stand-in for
|
||||
the `on_init` and `on_configuration_changed` event for modules. It is
|
||||
invoked on the first tick the server runs after the save has been
|
||||
patched, before most other events.
|
||||
|
||||
Use this event to initialize and/or migrate the data structures
|
||||
you need. Keep in mind that both mods and Clusterio modules can switch
|
||||
from any version to any version so it should be able to handle both
|
||||
forwards and backwards migrations gracefully.
|
||||
|
||||
#### clusterio_api.send_json(channel, data)
|
||||
|
||||
Send JSON data to Clusterio over the given channel. The `data` argument
|
||||
should be table that can be serialized with `game.table_to_json`.
|
||||
Clusterio plugins can listen to channels and will receive an event with
|
||||
the data sent here. See the [Communicating with Factorio
|
||||
section](writing-plugins.md#communicating-with-factorio) in the Writing
|
||||
Plugins document for more information.
|
||||
|
||||
If the game is started outside of Clusterio the data will be sent, but
|
||||
since there's no code following stdout to pick it up it will be lost.
|
||||
|
||||
**Note**: Payloads greater than 4 MB will cause stuttering in the game.
|
||||
|
||||
**Note**: This is not a binary safe way of sending data. Strings
|
||||
embedded into the tables sent must be valid UTF-8 text.
|
||||
|
||||
Parameters:
|
||||
- `channel`: string identifying which channel to send it on.
|
||||
- `data`: table that can be converted to JSON with game.table_to_json
|
||||
|
||||
|
||||
### serialize library
|
||||
|
||||
**require path**
|
||||
From a module
|
||||
`local serialize = require("modules/clusterio/serialize")`
|
||||
From a mod
|
||||
`local serialize = require("__clusterio_lib__/serialize")`
|
||||
|
||||
Serializes inventories and item stacks into a table that in turn can be
|
||||
converted into JSON with `game.table_to_json`. Properly handles the
|
||||
extra data for most items.
|
||||
|
||||
#### serialize.serialize_equipment_grid(LuaEquipmentGrid)
|
||||
|
||||
Returns a table containing a serialized representation of the equipment
|
||||
grid passed.
|
||||
|
||||
#### serialize.deserialize_equipment_grid(LuaEquipmentGrid, serialized_grid)
|
||||
|
||||
Deserialize a previously serialized equipment grid into the target
|
||||
LuaEquipmentGrid. Overwrites all content in the target.
|
||||
|
||||
#### serialize.serialize_item_stack(LuaItemStack, destination_table)
|
||||
|
||||
Serialize the LuaItemStack passed into the target destination_table.
|
||||
The destination_table should be a new empty table.
|
||||
|
||||
#### serialize.deserialize_item_stack(LuaItemStack, serialized_stack)
|
||||
|
||||
Deserialize a previously serialized item stack into the target
|
||||
LuaItemStack. Overwrites the content of the item stack.
|
||||
|
||||
#### serialize.serialize_inventory(LuaInventory)
|
||||
|
||||
Returns a table containing a serialized representation of the inventory
|
||||
passed.
|
||||
|
||||
#### serialize.deserialize_inventory(LuaInventory, serialized_inventory)
|
||||
|
||||
Deserialize a previously serialized inventory into the target
|
||||
LuaInventory. Overwrites slots that have content in the serialized
|
||||
inventory.
|
||||
|
||||
|
||||
Clusterio Plugins
|
||||
-----------------
|
||||
|
||||
See the [Writing Plugins](writing-plugins.md) document for details on
|
||||
the plugin interface and how to write plugins for Clusterio.
|
||||
@@ -0,0 +1,30 @@
|
||||
Save Patching
|
||||
-------------
|
||||
|
||||
In order to efficiently get data in and out of running Factorio servers
|
||||
and to provide custom interfaces Clusterio uses save
|
||||
patching<sup>[1]</sup> to add code to the game. The save patching is
|
||||
based on the built in
|
||||
[event_handler](https://github.com/wube/factorio-data/blob/master/core/lualib/event_handler.lua)
|
||||
library to Factorio and requires scenarios to be coded specifically for
|
||||
it. Mods on the other hand work without modifications.
|
||||
|
||||
This can be disabled with the `factorio.enable_save_patching` config on
|
||||
a per instance basis, however disabling it means that modules are not
|
||||
loaded into the game and most plugins will not function.
|
||||
|
||||
<sub>1: Before version 2.0 of Clusterio this was done at runtime
|
||||
with a patcher called Hotpatch, but due to the difficulty in supporting
|
||||
run time patching and the lack of documentation it was replace it with
|
||||
save patching instead.</sub>
|
||||
|
||||
|
||||
User Accounts
|
||||
-------------
|
||||
|
||||
The master server creates and stores accounts for all Factorio users
|
||||
that log into instances under its control. These accounts are used to
|
||||
store per player data and authorize management of the cluster. By
|
||||
default users accounts are assigned a Player role that gives a limited
|
||||
read access to the cluster resources, and admins of the cluster can be
|
||||
assigned an Admin role to get access to manage it.
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"plugins": [
|
||||
"plugins/markdown"
|
||||
],
|
||||
"tags": {
|
||||
"allowUnknownTags": false
|
||||
},
|
||||
"opts": {
|
||||
"destination": "docs/api",
|
||||
"recurse": true
|
||||
},
|
||||
"source": {
|
||||
"include": [
|
||||
"packages/master/src/",
|
||||
"packages/master/master.js",
|
||||
"packages/slave/slave.js",
|
||||
"packages/ctl/ctl.js",
|
||||
"packages/lib/",
|
||||
"plugins/"
|
||||
],
|
||||
"excludePattern": "node_modules"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
Managing a Cluster
|
||||
==================
|
||||
|
||||
Clusterio clusers are managed through the master server by using the
|
||||
`clusterioctl` command line interface which is invoked by running
|
||||
`npx clusterioctl <command>` in the clusterio directory. This document
|
||||
uses the shorthand `ctl> foo` to indicate `npx clusterioctl foo` should
|
||||
be executed. Mandatory parameters are shown in `<angles bracket>` and
|
||||
optional pameters are in `[square brackets]`.
|
||||
|
||||
Before `clusterioctl` can be used it needs to be configured for the
|
||||
cluster it will connect to. The easiest way to do this is to run
|
||||
`npx clusteriomaster bootstrap create-ctl-config <username>` on the
|
||||
master server, which creates the necessary `config-control.json` for
|
||||
managing the cluster as the given user.
|
||||
|
||||
|
||||
Slaves
|
||||
------
|
||||
|
||||
To be written.
|
||||
|
||||
|
||||
Instances
|
||||
---------
|
||||
|
||||
To be written.
|
||||
|
||||
|
||||
Users
|
||||
-----
|
||||
|
||||
Clusterio automatically creates user accounts for all players that join
|
||||
an instance when save patching is enabled. These accounts are used to
|
||||
store per player data shared between instances, like if the player
|
||||
should be whitelisted, admin or banned.
|
||||
|
||||
### List users
|
||||
|
||||
ctl> user list
|
||||
|
||||
Lists all user accounts in the cluster along with some data for each.
|
||||
|
||||
|
||||
### Create User
|
||||
|
||||
ctl> user create <name>
|
||||
|
||||
Creates a new empty user account for the given Factorio player. Note
|
||||
that case matters here. This is usually not required as user accounts
|
||||
are created automatically for players that join instances with save
|
||||
patching enabled.
|
||||
|
||||
|
||||
### Promote User to Server Admin
|
||||
|
||||
ctl> user set-admin <name> [--revoke] [--create]
|
||||
|
||||
Promotes the user given to in-game admin on instances with the
|
||||
`sync_adminlist` option enabled. If the `--revoke` switch is used the
|
||||
user is removed from the adminlist.
|
||||
|
||||
Since admin status is a part of the account data the account must exist
|
||||
for this to succeed, passing `--create` will create the account if it
|
||||
does not exist.
|
||||
|
||||
**Note:** Being a server admin does not grant any access to manage the
|
||||
cluster. See [set-roles](#set-cluster-roles) for adding roles which
|
||||
grant access to managing the clusetr.
|
||||
|
||||
|
||||
### Whitelist User
|
||||
|
||||
ctl> user set-whitelisted <name> [--remove] [--create]
|
||||
|
||||
Add the user given to the whitelist on instances with the
|
||||
`sync_whitelist` option enabled. If the `--remove` switch is used the
|
||||
user is removed from the whitelist.
|
||||
|
||||
Since whitelisted status is a part of the account data the account must
|
||||
exist for this to succeed, passing `--create` will create the account if
|
||||
it does not exist.
|
||||
|
||||
|
||||
### Ban User
|
||||
|
||||
ctl> user set-banned <name> [--reason <message>] [--pardon] [--create]
|
||||
|
||||
Ban user in-game from instances with the `sync_banlist` option enabled.
|
||||
Reason is a message that will be shown to the user when attemption to
|
||||
log in. If the `--pardon` switch is used it removes the ban.
|
||||
|
||||
Since ban status is a part of the account data the account must exist
|
||||
for this to succeed, passing `--create` will create the account if it
|
||||
does not exist.
|
||||
|
||||
Note: This bans the user from logging in to Factorio servers in the
|
||||
cluster, it does not revoke access to any cluster management they might
|
||||
have, see next section on setting roles for revoking tha.
|
||||
|
||||
|
||||
### Set Cluster Roles
|
||||
|
||||
ctl> user set-roles <name> [roles...]
|
||||
|
||||
Replaces the roles the user has in the cluster with a new list of roles.
|
||||
Calling with an empty roles argument will remove all roles from the
|
||||
user.
|
||||
|
||||
By default there's a roled named Admin which grants access to
|
||||
everything and a role named Player which grants a limited read access to
|
||||
the cluster, see [the section on roles](#roles) for more information
|
||||
about setting up roles and permissions.
|
||||
|
||||
|
||||
### Delete user
|
||||
|
||||
ctl> user delete <name>
|
||||
|
||||
Deletes everything stored on the master server for this user.
|
||||
|
||||
Note: If the player is banned from the cluster this will effectively
|
||||
unban them, as the ban status is stored with the user account.
|
||||
|
||||
Note: If the player joins the cluster again a new account will be made
|
||||
for them automatically.
|
||||
|
||||
|
||||
Roles
|
||||
-----
|
||||
|
||||
To be written.
|
||||
+287
@@ -0,0 +1,287 @@
|
||||
WebSocket protocol
|
||||
==================
|
||||
|
||||
<sub>This document describes the protocol used between the master and
|
||||
slaves, and unless you're developing Clusterio it's probably not going
|
||||
to be very useful for you.</sub>
|
||||
|
||||
The protocol is built on WebSocket and uses JSON encoded text messages
|
||||
to communicate to and from the master server. These messages contain
|
||||
a JSON encoded object with the following three properties:
|
||||
|
||||
- seq - integer|null - The sequence number of the message
|
||||
- type - string - The type of message
|
||||
- data - object - Message data
|
||||
|
||||
For the handshake and heartbeats the seq property is null.
|
||||
|
||||
|
||||
Handshake
|
||||
---------
|
||||
|
||||
Upon connecting to the WebSocket endpoint the server will respond with a
|
||||
`hello` message containing the master server version as the data. The
|
||||
client is then expected to send either `register_*` or a `resume`
|
||||
message to set up the connection with the master. The exact flow is
|
||||
described below.
|
||||
|
||||
|
||||
### Client connection flow
|
||||
|
||||
For a client making a new connecting to the master server the first
|
||||
course of action after obtaining a token is to connect with WebSocket to
|
||||
the /api/socket endpoint and wait for the server hello. After the
|
||||
hello, the client sends a register\_\* message corresponding to the type
|
||||
of the client. If successful the server will reply with a ready message
|
||||
and the connection has been established. See the flowchart below.
|
||||
|
||||

|
||||
|
||||
If for any reason the connection is closed or lost during the connection
|
||||
attempt the process is started over after a random delay, with the
|
||||
exception of the connection being closed with a 4003 code which means
|
||||
authentication failed and the client should stop trying to connect. The
|
||||
wait for hello and wait for ready states should also have a timeout,
|
||||
after which the connection is closed if the hello/reply wasn't received
|
||||
in time.
|
||||
|
||||
|
||||
### Client reconnection flow
|
||||
|
||||
If the connection is lost after the client has received the ready
|
||||
message from the server it should attempt a reconnect instead. This is
|
||||
very similar to a new connection, but instead of a register\_\* messsage
|
||||
the client sends a resume message and the server replies with a continue
|
||||
message if resuming the session was successful. This continue message
|
||||
has sequence number of the last message the master received and the
|
||||
client is expected to imediately send all messages from its send buffer
|
||||
that occured after the sequence number given.
|
||||
|
||||
The server will respond with an invalidate message if resuming the
|
||||
session is not possible. In this case the client should continue from
|
||||
the new session state of the connection flow (see the previous section).
|
||||
|
||||

|
||||
|
||||
Like the connection flow the process starts over after a delay if the
|
||||
connection is closed or lost. The same exception for connection closed
|
||||
with 4003 applies as with the connection flow. Additionally after an
|
||||
invalidate message has been received and the connection flow fails, it
|
||||
should start over from the connection flow.
|
||||
|
||||
|
||||
### Server connection flow
|
||||
|
||||
From the master server side new connections and reconnection attempts
|
||||
start out the same, a new connection has been made and the server sends
|
||||
the hello messoge to the client. If a vaild register\_\* message is
|
||||
received a new connection is set up for the client and the ready message
|
||||
is sent in reply. If a valid resume message is received and the session
|
||||
that's attempted to be resumed is active it'll signal that the session
|
||||
is resumed with a continue message and the client upon receiving the
|
||||
continue message should send the messages from its send buffer from
|
||||
after the last\_seq given in the continue message.
|
||||
|
||||
If the session token used to resume is not valid it'll send an invalidate
|
||||
message and expect the client to continue with a registr\_\* message. If an
|
||||
invalid or unexpected message is received during the handshake the connection
|
||||
is closed. See the flowchart below.
|
||||
|
||||

|
||||
|
||||
For authentication failure the connection is closed with code 4003,
|
||||
which the connection client should interpret as connecting will never
|
||||
succeed and stop trying to reconnect.
|
||||
|
||||
|
||||
### Connection tracking and heartbeats
|
||||
|
||||
Once connected as indicated by the ready and continue messages from the
|
||||
master server the client must start sending heartbeats at the interval
|
||||
given in the ready/continue message, and storing messages sent over the
|
||||
link in a sending side buffer. The heartbeats sent must have the seq of
|
||||
the last received message on the link and acts as an ack for received
|
||||
messages. The master server also sends heartbeats in the same manner
|
||||
and the client should remove messages up to and including the sequence
|
||||
number received in the heartbeat from its send buffer.
|
||||
|
||||
|
||||
Handshake messages
|
||||
------------------
|
||||
|
||||
### `hello`
|
||||
|
||||
Sent by the master server when the WebSocket connection has been opened.
|
||||
|
||||
- version - string - The version of the master, e.g. "2.0.0".
|
||||
- plugins - Object<string, string> -
|
||||
Object mapping plugin names to plugin version for plugins that are
|
||||
loaded on the master server.
|
||||
|
||||
### `register_slave`
|
||||
|
||||
Slave handshake for establishing a new connection session.
|
||||
|
||||
- token - string - Authentication token.
|
||||
- agent - string - Human readable string of the software connecting.
|
||||
- version - string - The protocol version of the slave, e.g. "2.0.0".
|
||||
- name - string - Name of the slave.
|
||||
- id - integer - ID of the slave.
|
||||
- plugins - Object<string, string> -
|
||||
Object mapping plugin names to plugin version for plugins that are
|
||||
available on the slave.
|
||||
|
||||
### `register_control`
|
||||
|
||||
Control handshake for enstablishing a new connection session.
|
||||
|
||||
- token - string - Authentication token.
|
||||
- agent - string - Human readable string of the software connecting.
|
||||
- version - string - The protocol version of the control, e.g. "2.0.0".
|
||||
|
||||
### `resume`
|
||||
|
||||
Handshake for resuming an existing connection session where the
|
||||
connection had dropped unexpectedly.
|
||||
|
||||
- session_token - string - Session token to resume session with.
|
||||
- last_seq - integer|null - Last message seq received by the client.
|
||||
|
||||
### `invalidate`
|
||||
|
||||
Sent by the master server in reply to a resume for a session that cannot
|
||||
be resumed. The client should start a new session with a register\_\*
|
||||
message in response.
|
||||
|
||||
### `continue`
|
||||
|
||||
Sent by the master server to signal resuming the session was successful.
|
||||
|
||||
- last_seq - integer|null - Last message seq received by the server.
|
||||
- heartbeat_interval - number - Rate client should send heartbeats at.
|
||||
|
||||
### `ready`
|
||||
|
||||
Sent by the master server to signal the connection has been established
|
||||
with a new session.
|
||||
|
||||
- session_token - string - Token to resume the session with.
|
||||
- heartbeat_interval - number - Rate client should send heartbeats at.
|
||||
|
||||
### `heartbeat`
|
||||
|
||||
Sent by both the master server and clients at the interval given in the
|
||||
continue/ready messages.
|
||||
|
||||
- last_seq - integer - Last message seq received over the link.
|
||||
|
||||
|
||||
Example handshake
|
||||
-----------------
|
||||
|
||||
Server hello after opening the WebSocket.
|
||||
|
||||
```json
|
||||
{
|
||||
"seq": null,
|
||||
"type": "hello",
|
||||
"data": {
|
||||
"version": "2.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Slave registering a new connection session.
|
||||
|
||||
```json
|
||||
{
|
||||
"seq": null,
|
||||
"type": "register_slave",
|
||||
"data": {
|
||||
"token": "xyzabc123",
|
||||
"agent": "Clusterio Slave",
|
||||
"version": "2.0.0",
|
||||
"name": "Foo",
|
||||
"id": 123
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Master server reply signaling the session is established and ready.
|
||||
|
||||
```json
|
||||
{
|
||||
"seq": null,
|
||||
"type": "ready",
|
||||
"data": {
|
||||
"session_token": "xyz.azy.bzy",
|
||||
"heartbeat_interval": 45
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Events
|
||||
------
|
||||
|
||||
Events are messages send in one direction over the link, that the
|
||||
receiver is expected to act upon, but not make any replies (for that
|
||||
there's [Requests](#requests). By convention message types ending in
|
||||
`*_event` are events. The data payload of an event consists solely of
|
||||
event specific properties.
|
||||
|
||||
See [packages/lib/link/messages.js](/packages/lib/link/messages.js) for
|
||||
the recognized events and their contents.
|
||||
|
||||
|
||||
Requests
|
||||
--------
|
||||
|
||||
Requests are messages which expects a response in return. They function
|
||||
a lot like HTTP requests except that both parties of a connection can
|
||||
inniate a request that is expected to be responded to. By convention
|
||||
message types ending in `*_request` are requests, and is expected to be
|
||||
replied to with a corresponding `*_response` message. If an error
|
||||
occured while processing the request an error response containing an
|
||||
error property in the data payload is sent instead.
|
||||
|
||||
See [packages/lib/link/messages.js](/packages/lib/link/messages.js) for
|
||||
the recognized requests and their contents.
|
||||
|
||||
### `*_response`
|
||||
|
||||
- seq - integer - The seq number of the request message.
|
||||
- additional properties may be present depending on the type of request.
|
||||
|
||||
### `*_response` error
|
||||
|
||||
- seq - integer - The seq number of the request message.
|
||||
- error - string - Human readable text message describing the error.
|
||||
|
||||
|
||||
Graceful Disconnect
|
||||
-------------------
|
||||
|
||||
To ensure no data is lost when closing the link it's important to
|
||||
send a prepare disconnect request and wait for it to complete before
|
||||
closing the WebSocket. Either party of the link can send this request
|
||||
and the party sending the request should be the one that initates the
|
||||
close of the WebSocket.
|
||||
|
||||
### `prepare_disconnect_request`
|
||||
|
||||
Request graceful disconnect of the connection. The receiving party should
|
||||
stop tasks that send events and requests over the link and wait for
|
||||
pending requests it has over the link to complete, but it should not
|
||||
stop responding to requests sent to it. Only after this process is done
|
||||
should the response be sent.
|
||||
|
||||
### `prepare_disconnect_response`
|
||||
|
||||
Signals the preparation for disconnect is complete and that the
|
||||
connection can be closed. The party that initiatied the prepare
|
||||
disconnect request may still send other requests/events over the link to
|
||||
finish its part of the disconnection proceedure.
|
||||
|
||||
If the error response variant is received it should be logged if
|
||||
possible and treated as a success.
|
||||
@@ -0,0 +1,661 @@
|
||||
Writing Plugins
|
||||
===============
|
||||
|
||||
Plugins for Clusterio are classes written in JavaScript that run under
|
||||
Node.js. The plugin classes have pre-defined hooks that are called
|
||||
during various stages and operations of Clusterio.
|
||||
|
||||
|
||||
Contents
|
||||
--------
|
||||
|
||||
- [Plugin Structure](#plugin-structure)
|
||||
- [Defining the plugin class](#defining-the-plugin-class)
|
||||
- [Plugin Configuration](#plugin-configuration)
|
||||
- [Communicating with Factorio](#communicating-with-factorio)
|
||||
- [Defining Link Messages](#defining-link-messages)
|
||||
- [Defining Events](#defining-events)
|
||||
- [Defining Requests](#defining-requests)
|
||||
- [Sending Link Messages](#sending-link-messages)
|
||||
- [Handling connection events](#handling-connection-events)
|
||||
- [Collecting Statistics](#collecting-statistics)
|
||||
- [Adding Custom Commands to clusterioctl](#adding-custom-commands-to-clusterioctl)
|
||||
|
||||
|
||||
Plugin Structure
|
||||
----------------
|
||||
|
||||
The basic file structure of a plugin is the following.
|
||||
|
||||
plugin_name/
|
||||
+- info.js
|
||||
+- package.json
|
||||
+- master.js
|
||||
+- instance.js
|
||||
+- control.js
|
||||
+- test/
|
||||
| +- plugin.js
|
||||
+- module/
|
||||
+- module.json
|
||||
+- plugin.lua
|
||||
|
||||
Clusterio plugins are Node.js packages that can be published on npm for
|
||||
ease of installation and distribution. The usual guides for creating
|
||||
such packages apply. At minimum the `package.json` file must contain a
|
||||
version entry.
|
||||
|
||||
A possible workflow for developing plugins is to place the plugin in a
|
||||
sub-directory of where clusterio has been installed, and rely on Node.js
|
||||
searching up the folder heirarchy for it to find `@clusterio/lib`. To
|
||||
add it to `plugin-list.json` so that it gets loaded use the `plugin add
|
||||
<path>` sub-command to either clusteriomaster, clusterioslave or
|
||||
clusterioctl. Note that it's important that the path starts with ./ or
|
||||
../ (use .\ or ..\ on Windows).
|
||||
|
||||
For a plugin the most important file is the `info.js` file. Without it
|
||||
the plugin will not recognized by Clusterio. Here's an example of it:
|
||||
|
||||
const libLink = require('@clusterio/lib/link'); // For messages
|
||||
|
||||
module.exports = {
|
||||
name: "foo_frobber",
|
||||
title: "Foo Frobber",
|
||||
description: "Does advanced frobnication",
|
||||
instanceEntrypoint: "instance",
|
||||
masterEntrypoint: "master",
|
||||
messages: {
|
||||
/* See below */
|
||||
},
|
||||
};
|
||||
|
||||
The following properties are recognized:
|
||||
|
||||
**name**:
|
||||
Internal name of the plugin. Must be the same as the plugin's
|
||||
directory name
|
||||
|
||||
**title**:
|
||||
Name of the plugin as shown to users. Currently not used.
|
||||
|
||||
**description**:
|
||||
Brief description of what the plugin does. Currently not used.
|
||||
|
||||
**instanceEntrypoint**:
|
||||
Path to a Node.js module relative to the plugin directory which
|
||||
contains the InstancePlugin class definition for this plugin. This
|
||||
is an optional paramater. A plugin may have code only for instances
|
||||
but it must still be loaded on the master in order for it to be
|
||||
possible to load it on an instance.
|
||||
|
||||
**InstanceConfigGroup**:
|
||||
Subclass of `PluginConfigGroup` for defining the per instance
|
||||
configuration fields for this plugin. See [Plugin
|
||||
Configuration](#plugin-configuration)
|
||||
|
||||
**masterEntrypoint**:
|
||||
Path to a Node.js module relative to the plugin directory which
|
||||
contains the MasterPlugin class definiton for this plugin. This is
|
||||
an optional parameter. A plugin can be made that only runs on the
|
||||
master server.
|
||||
|
||||
**MasterConfigGroup**:
|
||||
Subclass of `PluginConfigGroup` for defining the master server
|
||||
configuration fields for this plugin. See [Plugin
|
||||
Configuration](#plugin-configuration)
|
||||
|
||||
**controlEntrypoint**:
|
||||
Path to a Node.js module relative to the plugin directory which
|
||||
contains the ControlPlugin class definition for this plugin. This
|
||||
is an optional paramater. A plugin can be made that only runs on
|
||||
the clusterioctl side.
|
||||
|
||||
**messages**:
|
||||
Object with link messages definitions for this plugin. See guide
|
||||
for [defining link messages](#defining-link-messages) below.
|
||||
|
||||
The optional module folder contains a Clusterio module that will be
|
||||
patched into the save when the plugin is enabled. See the section on
|
||||
[Clusterio Modules](developing-for-clusterio.md) in the Developing for
|
||||
Clusterio document. The only restriction imposed on modules embedded
|
||||
into plugins is that they must be named the same as the plugin.
|
||||
|
||||
While there is no standard for how to organize a plugin it's recommended
|
||||
to put the MasterPlugin class definition into master.js and the
|
||||
InstancePlugin class definition into instance.js. You can put them into
|
||||
whatever file you want (even the same one for both).
|
||||
|
||||
For both instanceEntrypoint and masterEntrypoint the path should not end
|
||||
with .js and it should use forward slashes for directory separators if
|
||||
any.
|
||||
|
||||
|
||||
Defining the plugin class
|
||||
-------------------------
|
||||
|
||||
The plugin class should derive from its respective base class defined in
|
||||
`lib/plugin`. For example, to define a MasterPlugin class the following
|
||||
code can be used:
|
||||
|
||||
const libPlugin = require('@clusterio/lib/plugin');
|
||||
|
||||
class MasterPlugin extends libPlugin.BaseMasterPlugin {
|
||||
async init() {
|
||||
this.foo = 42;
|
||||
await this.startFrobnication();
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
MasterPlugin,
|
||||
}
|
||||
|
||||
For the instance plugin it's exactly the same except "Master" is
|
||||
replaced with "Instance", and for the clusterioctl plugin "Control" is
|
||||
used. The available hooks that you can override are documented in the
|
||||
base class [in lib/plugin.js](/packages/lib/plugin.js).
|
||||
|
||||
It's best to avoid defining a constructor, but if you insist on defining
|
||||
one, forward all arguments to the base class. E.g.:
|
||||
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
// Code here
|
||||
}
|
||||
|
||||
The arguments passed may change, and attempting to modify them will
|
||||
result in unpredicatable behaviour. The async init method is always called
|
||||
immediatly after the constructor, so there's little reason to do this.
|
||||
|
||||
|
||||
Defining Configuration
|
||||
----------------------
|
||||
|
||||
Clusterio provides a configuration system that handles storing,
|
||||
distributing, editing and validating config fields for you. You can
|
||||
take advantage of it by subclassing `PluginConfigGroup`, setting the
|
||||
`groupName` to your plugin name, defining fields on it, finalizing it,
|
||||
and passing it as either `MasterConfigGroup` or `InstanceConfigGroup` in
|
||||
the `info.js` export. For example in info.js:
|
||||
|
||||
const libConfig = require("@clusterio/lib/config");
|
||||
|
||||
class MasterConfigGroup extends libConfig.PluginConfigGroup { }
|
||||
MasterConfigGroup.groupName = "foo_frobber";
|
||||
MasterConfigGroup.define({
|
||||
name: "level",
|
||||
description: "Level of frobnication done",
|
||||
type: "number",
|
||||
initial_value: 2,
|
||||
});
|
||||
MasterConfigGroup.finalize();
|
||||
|
||||
module.exports = {
|
||||
...
|
||||
MasterConfigGroup: MasterConfigGroup,
|
||||
};
|
||||
|
||||
Code inside the `MasterPlugin` class will then be able to access the
|
||||
level config field through the `Config` object at `this.master.config`,
|
||||
for example in the MasterPluginClass:
|
||||
|
||||
async init() {
|
||||
let level = this.master.config.get("foo_frobber.level");
|
||||
console.log("I got a frobnication level of", level);
|
||||
}
|
||||
|
||||
The same applies for instance configs, replace "master" with "instance"
|
||||
where appropriate. See [Configuration System](configuration-system.md)
|
||||
for more details on how this system works.
|
||||
|
||||
|
||||
### Handling Invalid Configuration
|
||||
|
||||
If the plugin requires a certain feature to be enabled to function it
|
||||
should throw an error during init if this is not the case. The most
|
||||
common such feature is the save patching, which can be disabled to run
|
||||
vanilla or scenarios not compatible with Clusterio. For example:
|
||||
|
||||
async init() {
|
||||
if (!this.instance.config.get("factorio.enable_save_patching")) {
|
||||
throw new Error("foo_frobber plugin requires save patching.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Communicating with Factorio
|
||||
---------------------------
|
||||
|
||||
For pushing data into Factorio there's RCON, which lets you send
|
||||
arbitrary Lua commands to invoke whatever code you want in the game.
|
||||
This is done by calling the `sendRcon` method on the instance's server object.
|
||||
For example:
|
||||
|
||||
async onStart() {
|
||||
let response = await this.instance.server.sendRcon(
|
||||
"/sc rcon.print('data')"
|
||||
);
|
||||
|
||||
// Do stuff with response.
|
||||
}
|
||||
|
||||
|
||||
Because data into Factorio is streamed at a rate of 3-6 kB/s by default,
|
||||
it is recommended to avoid sending large commands as much as possible,
|
||||
and to strip down the data on the ones you send to only what's strictly
|
||||
necessary. You can have lua code injected into the game via the module
|
||||
system and call that from RCON to avoid having to send code through the
|
||||
commands.
|
||||
|
||||
For getting data out from Factorio there's both RCON and the `send_json`
|
||||
API of the Clusterio module. Returning data via RCON is prefered if the
|
||||
action is initiated from the Node.js side. The `send_json` API allows
|
||||
sending JSON payloads on channels that plugins can listen to. From a
|
||||
plugin you listen for an event named `ipc-channel_name` in order to get
|
||||
data sent by `send_json`. For example in the plugin code:
|
||||
|
||||
async init() {
|
||||
this.instance.server.on('ipc-my_plugin_foo', content =>
|
||||
this.handleFoo(content).catch(err => console.log(
|
||||
"Error handling foo:", foo
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
async handleFoo(content) {
|
||||
// Do stuff with content
|
||||
}
|
||||
|
||||
And then in the module for the plugin:
|
||||
|
||||
local clusterio_api = require('modules/clusterio/api')
|
||||
|
||||
-- inside some event handler
|
||||
clusterio_api.send_json('my_plugin_foo', { data = 123 })
|
||||
|
||||
It's recommended to either use the plugin name as the channel name or to
|
||||
prefix the channel name with the name of the plugin if you need multiple
|
||||
channels. It's also important to catch any errors that might occur as
|
||||
they will otherwise be propogated to the instance code and kill the
|
||||
server.
|
||||
|
||||
Data out from Factorio does not have the same limits as data into
|
||||
Factorio, RCON responses can be in 100kB range without causing issues,
|
||||
and payloads to the `send_json` API can be in the 4MB range provided the
|
||||
server has a fast enough storage system.
|
||||
|
||||
**Note:** both `send_json` and RCON can operate out of order. For
|
||||
`send_json` it's possible that payloads greater than 4kB are received
|
||||
after payloads that were sent at a later point in time. For RCON,
|
||||
commands longer than 50 characters may end up being executed after
|
||||
shorter commands sent after it.
|
||||
|
||||
|
||||
Defining Link Messages
|
||||
----------------------
|
||||
|
||||
You will most likely have to communicate with the master or other
|
||||
instances in your plugin for it to do anything useful. For this there's
|
||||
a WebSocket communication channel established between the slaves and the
|
||||
master server that plugins can define their own messages to send over
|
||||
it. This channel is bi-directional and all messages sent over it are
|
||||
validated with a JSON schema (see [this guide][guide] for an
|
||||
introduction to writing JSON schema).
|
||||
|
||||
[guide]: https://json-schema.org/learn/getting-started-step-by-step.html
|
||||
|
||||
There are currently two kinds of messages that can be defined: events
|
||||
and requests. Events are simple one-way notifications that invoke a
|
||||
handler on the target it's sent to. Requests are pairs of request and
|
||||
response messages where the request is sent to the target and the
|
||||
response is the reply back from the target. The requests are similar to
|
||||
HTTP requests, in that both parties of a link may initiate one.
|
||||
|
||||
|
||||
### Defining Events
|
||||
|
||||
Events are defined as properties of the messages object exported by
|
||||
`info.js` that map to instances of the `Event` class from `lib/link`.
|
||||
The name of the property correspond to the handler invoked on the plugin
|
||||
class. The Event constructor takes an object of properties that define
|
||||
the event, for example the following could be defined in `info.js`:
|
||||
|
||||
messages: {
|
||||
startFrobnication: new libLink.Event({
|
||||
type: 'foo_frobber:start_frobnication',
|
||||
links: ['master-slave', 'slave-instance'],
|
||||
forwardTo: 'instance',
|
||||
eventProperties: {
|
||||
"frobnication_type": { type: "string" },
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
This specifies an event that can be sent from the master to a slave,
|
||||
and from a slave to an instance. It also specifies that the event must
|
||||
contain the property `frobnication_type`, with a string value in the
|
||||
data payload. It will also be forwarded by slaves to a specific
|
||||
instance.
|
||||
|
||||
The following properties are recognized by the Event constructor:
|
||||
|
||||
#### type
|
||||
|
||||
The message type sent over the wire. This must start with the name of
|
||||
the plugin followed by colon and and be unique for the plugin. The type
|
||||
of the message sent over the socket will have the suffix `_event`
|
||||
appended to it.
|
||||
|
||||
#### links
|
||||
|
||||
An array of strings describing which links this event can be sent over.
|
||||
Direction matters, `'master-slave'` means the event can be sent from the
|
||||
master to the slave, but can't be sent back the other way, unless
|
||||
`'slave-master'` is also present in the links array.
|
||||
|
||||
The available endpoints are `master`, `slave`, `instance`, and
|
||||
`control`. Master talks with slave and control, and slave talks to
|
||||
instance. The full chain must be specified as the individual links in
|
||||
order for a message to travers multiple hops, (i.e., for a message to go
|
||||
from master to instance it must have both `'master-slave'` and
|
||||
`'slave-instance'` in the links array). See `forwardTo` and
|
||||
`broadcastTo` for ways to forward an event to the next link in a chain.
|
||||
|
||||
#### forwardTo
|
||||
|
||||
Target to forward an event to. Can either be `'master'`, to indicate a
|
||||
slave should forward it to the master server, or `'instance'`, to
|
||||
indicate it should be forwarded to the instances specified by the
|
||||
`instance_id` event property. This works by using a default handler for
|
||||
the event at the links that forward it.
|
||||
|
||||
#### broadcastTo
|
||||
|
||||
Target to broadcast this message towards. A value of "instance" means
|
||||
the event will be broadcast to all instances downstream of the target
|
||||
it's sent to, but not back from where it came from. Currently, only
|
||||
"instance" is supported. This means that sending an event to a slave
|
||||
that slave except for the instance it came from. from an instance will
|
||||
cause it to be broadcast to all instances of
|
||||
|
||||
#### eventProperties
|
||||
|
||||
Object with properties mapping to a JSON schema of that property that
|
||||
specifies what's valid to send in the event. This is equivalent to
|
||||
using the `properties` keyword in JSON schema, except that the
|
||||
properties specified are implicitly required and additional properties
|
||||
are not allowed. See [this guide][guide] for an introduction to writing
|
||||
JSON schemas.
|
||||
|
||||
The forwardTo and broadcastTo can be combined such that specifying
|
||||
`'master'` as the forwardTo value and `'instance'` as the broadcastTo
|
||||
value will cause the event to be broadcast to all instances in the
|
||||
cluster. For this to work, you will need to specify `instance-slave`,
|
||||
`slave-master`, `master-slave`, and `slave-instance` as the links.
|
||||
|
||||
Keep in mind when forwarding events that if the target an event is being
|
||||
forwarded to is not online, the event will be dropped. Use a request if
|
||||
you need a confirmation that the message was received.
|
||||
|
||||
### Definining Requests
|
||||
|
||||
Requests are defined as properties of the messages object exported by
|
||||
`info.js` that map to instances of the `Request` class from `lib/link`.
|
||||
The name of the property corresponds to the handler invoked on the plugin
|
||||
class. The Request constructor takes an object of properties that define
|
||||
the event. For example, the following could be defined in `info.js`:
|
||||
|
||||
messages: {
|
||||
reportFrobnication: new libLink.Request({
|
||||
type: 'foo_frobber:report_frobnication',
|
||||
links: ['master-slave', 'slave-instance'],
|
||||
forwardTo: 'instance',
|
||||
requestProperties: {
|
||||
"verbosity": { type: "integer" },
|
||||
},
|
||||
responseProperties: {
|
||||
"report": {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
This specifies a request that can be sent from the master to a slave,
|
||||
and from a slave to an instance. The request data must contain the
|
||||
property `verbosity` with an integer number as the value, as well as the
|
||||
`instance_id` property (implied by `forwardTo: 'instance'`), and the
|
||||
response sent must contain a `report` property mapping to an array of
|
||||
strings. When received by a slave, it will also be forwarded to the
|
||||
instance specified by `instance_id`.
|
||||
|
||||
The following properties are recognized by the Request constructor:
|
||||
|
||||
#### type
|
||||
|
||||
The message type sent over the wire. This must start with the name of
|
||||
the plugin followed by colon and and be unique for the plugin. The type
|
||||
of the message sent over the socket will have the suffix `_request`
|
||||
appended to it for the request and `_response` appended to it for the
|
||||
response.
|
||||
|
||||
#### links
|
||||
|
||||
An array of strings describing which links this request can be sent
|
||||
over. Direction matters; `'master-slave'` means the request can be sent
|
||||
from the master to the slave and the slave can reply to the master, but
|
||||
the slave can't send a request to the master unless `'slave-master'` is
|
||||
also present in the links array.
|
||||
|
||||
The available endpoints are `master`, `slave`, `instance`, and
|
||||
`control`. Master talks with slave and control, and slave talks to
|
||||
instance. The full chain must be specified as the individual links in
|
||||
order for a message to travers multiple hops, (i.e., for a message to go
|
||||
from master to instance it must have both `'master-slave'` and
|
||||
`'slave-instance'` in the links array). See `forwardTo` for ways to
|
||||
forward a request to the next link in a chain.
|
||||
|
||||
#### forwardTo
|
||||
|
||||
Target to forward the request to. Can either be `'master'` to indicate
|
||||
a slave should forward it to the master server when receiving it from an
|
||||
instance, or `'instance'` to indicate it should be forwarded to the
|
||||
instances specified by the `instance_id` request property. This works
|
||||
by using a default handler for the request by the links that forward it.
|
||||
|
||||
#### requestProperties
|
||||
|
||||
Object with properties mapping to a JSON schema of that property that
|
||||
specifies what's valid to send in the request. This is equivalent to
|
||||
using the `properties` keyword in JSON schema, except that the
|
||||
properties specified are implicitly required and additional properties
|
||||
are not allowed. See [this guide][guide] for an introduction to writing
|
||||
JSON schemas
|
||||
|
||||
#### responseProperties
|
||||
|
||||
Same as the requestProperties only for the response sent back by the
|
||||
target.
|
||||
|
||||
|
||||
Sending Link Messages
|
||||
---------------------
|
||||
|
||||
Link messages are sent by calling the `.send()` method on the
|
||||
Event/Request instance with the link you want to send it over and the
|
||||
data to send. For `InstancePlugin` code the link to the slave is the
|
||||
`instance` itself, which is accessible through the `.instance` property
|
||||
of the `InstancePlugin`. The `.info` property of the plugin class exposes
|
||||
the data exported from the plugin's `info.js` module. In other words:
|
||||
|
||||
// In an InstancePlugin class
|
||||
async frobnicate() {
|
||||
this.info.messages.exampleEvent.send(this.instance, { foo: "bar" });
|
||||
}
|
||||
|
||||
For the Request class the send method is async and returns the response
|
||||
data received from the target it was sent to, or throws an error if the
|
||||
request failed.
|
||||
|
||||
|
||||
### Handling connection events
|
||||
|
||||
There are a few connection related events that plugins neeed to repsond
|
||||
to in order to avoid data loss and connection problems. The most
|
||||
important is the prepare disconnect for the link between master and
|
||||
slave. This is signaled to `MasterPlugin` classes via the
|
||||
`onPrepareSlaveDisconnect` hook and to `InstancePlugin` classes via the
|
||||
`onPrepareMasterDisconnect` hook.
|
||||
|
||||
After the prepare disconnect the connection will be closed, which will
|
||||
result in pending requests and events being dropped. Plugins must
|
||||
respond to the prepare disconnect by stopping any processess it does
|
||||
that send events or requests over the link in question. This can be
|
||||
accomplished either through listening for the prepare disconnect hook,
|
||||
or by checking the `connected` or `closing` property of the connector
|
||||
for the master/slave connection. For example the sending of an event
|
||||
from an `InstancePlugin` class can be stopped while the connection is
|
||||
closing by using the following code:
|
||||
|
||||
if (!this.slave.connector.closing) {
|
||||
this.info.messages.frobnicate.send(this.instance, { foo: "bar" });
|
||||
}
|
||||
|
||||
If the event or request needs to be sent to the master it can be put
|
||||
into a queue stored on the plugin instance and sent out when the
|
||||
connection is established again. The re-establishement of the
|
||||
connection is notified to plugins via the `connect` event to the
|
||||
`onMasterConnectionEvent` and `onSlaveConnectionEvent` hooks.
|
||||
|
||||
The second connection event which is of lesser importance to respond to
|
||||
is the `drop` connection event served through `onMasterConnectionEvent`
|
||||
for `InstancePlugin` classes and through `onSlaveConnectionEvent` for
|
||||
`MasterPlugin` classes. This is raised when the connection between the
|
||||
master and slave in question is lost, most likely due to networking
|
||||
issues. When in the dropped state the slave will keep trying to
|
||||
reconnect to the master server in order to re-establish it, and if
|
||||
successful no events or requests will be lost. However while in the
|
||||
dropped state any requests and events sent gets queued up in memory
|
||||
until the connection is re-established. This means that if your plugin
|
||||
sends a lot of events or requests, they can end up being queued up in a
|
||||
buffer for a long time and sent out all at once. To avoid this you
|
||||
should be throtteling and/or stopping your requests/events after `drop`
|
||||
has been raised, and continue back as normal when `connect` is raised.
|
||||
|
||||
|
||||
Collecting Statistics
|
||||
---------------------
|
||||
|
||||
Clusterio comes with its own Prometheus client implementation, one part
|
||||
due to Not Invented Here and another part due to collectors in
|
||||
prom-client being difficult to get to work nicely with collecting data
|
||||
from plugins optionally loaded at runtime on different computers.
|
||||
|
||||
In its simplest form collecting data from plugins consists of defining
|
||||
the metric and updating it somewhere in the plugin code. For example:
|
||||
|
||||
const { Counter } = require('@clusterio/lib/prometheus');
|
||||
|
||||
const fooMetric = new Counter(
|
||||
'clusterio_foo_frobber_foo_metric', "Measures the level of foo",
|
||||
);
|
||||
|
||||
// Somewhere in the master plugin code
|
||||
fooMetric.inc();
|
||||
|
||||
This works for master plugins, and the metric will be automatically
|
||||
available through the /metric HTTP endpoint. It's recommended that
|
||||
plugin metrics follow `clusterio_<plugin_name>_<metric_name>` as the
|
||||
naming scheme.
|
||||
|
||||
For metrics that are per-instance, you must define an `instance_id` label and
|
||||
set it accordingly, for example:
|
||||
|
||||
const { Counter } = require('@clusterio/lib/prometheus');
|
||||
|
||||
const barMetric = new Gauge(
|
||||
'clusterio_foo_frobber_bar_metric', "Bar instance level",
|
||||
{ labels: ['instance_id'] }
|
||||
);
|
||||
|
||||
// Somewhere in the instance plugin code
|
||||
barMetric.labels(String(this.instance.config.get("instance.id"))).set(someValue);
|
||||
|
||||
Metrics are automatically registered to the default registry, and this
|
||||
default registry is automatically polled by the master server on slaves.
|
||||
This means that it's important that you place the definition of the
|
||||
metric at module level so that it's not created more than once over the
|
||||
lifetime of a slave. Since the metrics remember their values and would
|
||||
continue to be exported after an instance is shutdown, there's code at
|
||||
instance shutdown that removes all the values where the `instance_id`
|
||||
label matches the id of the instance shut down.
|
||||
|
||||
For statistics you need to update on collection there's an `onMetrics`
|
||||
hook on both master and instance plugins that is run before the
|
||||
metrics in the default registry are collected.
|
||||
|
||||
|
||||
Adding Custom Commands to clusterioctl
|
||||
--------------------------------------
|
||||
|
||||
The control entrypoint for plugins allows you to extend clustectl with
|
||||
your own commands. The creation of custom commands typically starts
|
||||
with defining a command tree for the plugin:
|
||||
|
||||
const { Command, CommandTree } = require("@clusterio/lib/command");
|
||||
const fooFrobberCommands = new CommandTree({
|
||||
name: "foo-frobber", description: "Foo Frobber Plugin commands"
|
||||
});
|
||||
|
||||
Then commands are added to the the plugin's command tree:
|
||||
|
||||
const info = require("./info");
|
||||
|
||||
fooFrobberCommands.add(new Command({
|
||||
definition: ["frobnicate <type>", "Do frobnications", (yargs) => {
|
||||
yargs.positional("level", {
|
||||
describe: "type of frobnication", type: "string"
|
||||
});
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
await info.messages.frobnicate.send(control, {
|
||||
instance_name: "Console",
|
||||
content: args.message,
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
For a command the `definition` is the arguments to pass to
|
||||
[yargs.command](http://yargs.js.org/docs/#api-reference-commandcmd-desc-builder-handler)
|
||||
(see also
|
||||
[yargs.positional](http://yargs.js.org/docs/#api-reference-positionalkey-opt)
|
||||
and
|
||||
[yargs.options](http://yargs.js.org/docs/#api-reference-optionskey-opt)
|
||||
for setting up positional and optional arguments to commands). The
|
||||
`handler` is an async function that's invoked when the command is
|
||||
executed and it's passed the parsed command line arguments and a
|
||||
reference to the `Control` class of clusterioctl. It's possible to optain
|
||||
a reference to the plugin class with `control.plugins.get(info.name)`.
|
||||
|
||||
Note that messages sent from clusterioctl needs to have
|
||||
`"control-master"` as a part of the links array for it to be accepted by
|
||||
the master server, see [Defining Link Messages](#defining-link-messages)
|
||||
for how to define the messages that can be sent to the master.
|
||||
|
||||
To have the command tree become part of clusterioctl it needs to be added
|
||||
to the rootCommand tree in `addCommands` callback of the Control plugin:
|
||||
|
||||
const libPlugin = require("@clusterio/lib/plugin");
|
||||
|
||||
class ControlPlugin extends libPlugin.BaseControlPlugin {
|
||||
async addCommands(rootCommand) {
|
||||
rootCommand.add(fooFrobberCommands);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ControlPlugin,
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"packages": [
|
||||
"packages/*",
|
||||
"plugins/*"
|
||||
],
|
||||
"version": "2.0.0-alpha.0"
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const NodeInstance_1 = require("./NodeInstance");
|
||||
const Point_1 = require("./Point");
|
||||
const Grid_1 = require("./Grid");
|
||||
/**
|
||||
* Manages all nodes within the cluster
|
||||
*/
|
||||
class ClusterManager {
|
||||
constructor() {
|
||||
this.Grid = new Grid_1.Grid();
|
||||
}
|
||||
/**
|
||||
* Generates a x*y grid of 1x1 nodes
|
||||
* @param width How wide to make the node grid
|
||||
* @param height How high to make the node grid
|
||||
* @param nodeNamePrefix The prefix used for the initial node names. The name will be in the format <prefix>_x_y.
|
||||
*/
|
||||
GenerateBasicGridOfNodes(width, height, nodeNamePrefix) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
for (let y = 0; y < height; y++) {
|
||||
let newNode = new NodeInstance_1.NodeInstance(nodeNamePrefix + "-" + (x + 1) + "-" + (y + 1), new Point_1.Point(x, y), 1, 1);
|
||||
this.Grid.AddNodeToGrid(newNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Assigns all nodes that currently do not have a server assigned to the given server
|
||||
* @param server Server to assign all unassigned nodes to
|
||||
*/
|
||||
AssignAllUnassignedNodesToServer(server) {
|
||||
this.CheckServerArray(server);
|
||||
this.Grid.GetNodes()
|
||||
.filter(node => { node.Server === null; })
|
||||
.forEach(node => {
|
||||
server.AddNodeInstance(node);
|
||||
node.Server = server;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Assigns a node to a server
|
||||
* @param x x co-ordinate of the node
|
||||
* @param y y co-ordinate of the node
|
||||
* @param serverName server name
|
||||
*/
|
||||
AssignNodeToServer(x, y, server) {
|
||||
this._nodeArray[x][y].Server = server;
|
||||
}
|
||||
CheckServerArray(server) {
|
||||
let s = this._servers[server.Name];
|
||||
if (s == null) {
|
||||
this._servers[server.Name] = server;
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.ClusterManager = ClusterManager;
|
||||
//# sourceMappingURL=ClusterManager.js.map
|
||||
@@ -1,112 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const child_process = require("child_process");
|
||||
const needle = require("needle");
|
||||
const Helpers_1 = require("./Helpers");
|
||||
const fs = require("fs");
|
||||
const util = require("util");
|
||||
/**
|
||||
* Contains all interactions between the cluster model, the
|
||||
* Clusterio master.js/client.js infrastructure and the operating system.
|
||||
*/
|
||||
class ClusterioMasterProxy {
|
||||
constructor(masterIP, masterPort, masterAuthToken, serviceHostType) {
|
||||
this._masterIP = `${masterIP}:${masterPort}`;
|
||||
this._needleOptionsWithTokenAuthHeader = {
|
||||
headers: {
|
||||
'x-access-token': masterAuthToken,
|
||||
},
|
||||
};
|
||||
}
|
||||
async RunRconCommand(instanceId, command, logDescription) {
|
||||
console.log("Running command: " + logDescription);
|
||||
let request = { instanceID: instanceId, command };
|
||||
let response = await needle("post", this._masterIP + "/api/runCommand", request, this._needleOptionsWithTokenAuthHeader);
|
||||
}
|
||||
async GetSlaves() {
|
||||
let response = await needle("get", this._masterIP + "/api/slaves", this._needleOptionsWithTokenAuthHeader);
|
||||
let body = response.body;
|
||||
let slaves = new Array();
|
||||
Object.entries(body).forEach((keyValuePair, index, array) => {
|
||||
let slave = keyValuePair[1];
|
||||
slave.id = keyValuePair[0];
|
||||
slaves.push(slave);
|
||||
});
|
||||
return slaves;
|
||||
}
|
||||
async CreateNodeInstancesOnLocalServer(nodes) {
|
||||
await Helpers_1.asyncForEach(nodes, async (node) => {
|
||||
await this.CreateNodeInstanceOnLocalServer(node);
|
||||
});
|
||||
await Helpers_1.asyncForEach(nodes, async (node) => {
|
||||
await this.CreateTeleportZonesForANodeInstance(node);
|
||||
});
|
||||
await this.sleep(1000);
|
||||
await Helpers_1.asyncForEach(nodes, async (node) => {
|
||||
await this.CreateTeleportRestrictionsForANodeInstance(node);
|
||||
await this.sleep(100);
|
||||
});
|
||||
}
|
||||
async CreateNodeInstanceOnLocalServer(node) {
|
||||
console.log(`Creating instance ${node.Name}...`);
|
||||
await this.CreateNodeInstanceOnLocalServerUsingPm2(node);
|
||||
console.log("Waiting for node to connect to cluster master...");
|
||||
while (true) {
|
||||
await this.sleep(1000);
|
||||
//TODO: Use /api/getSlaveMeta/
|
||||
let slaves = await this.GetSlaves();
|
||||
let slave = slaves.find((slave, index, array) => slave.instanceName === node.Name);
|
||||
if (slave != null) {
|
||||
node.ClusterioWorldId = slave.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
async CreateNodeInstanceOnLocalServerUsingPm2(node) {
|
||||
let proc = child_process.spawn("pm2", ['start', '--name', node.Name, 'client.js', '--', 'start', node.Name]);
|
||||
proc.stdout.on('data', (data) => { console.log(`stdout: ${data}`); });
|
||||
proc.stderr.on('data', data => { console.log(`stderr: ${data}`); });
|
||||
proc.on('error', error => { console.log(`error: ${error}`); });
|
||||
}
|
||||
async CreateNodeInstanceOnLocalServerUsingScreen(node) {
|
||||
// first call of client.js start for a new node creates map from HotPatch scenario then exits.
|
||||
let proc = child_process.spawn("screen", ['-dmS', node.Name, '-L', '-Logfile', node.Name + '.log', 'node', 'client.js', 'start', node.Name]);
|
||||
proc.stdout.on('data', (data) => { console.log(`stdout: ${data}`); });
|
||||
proc.stderr.on('data', data => { console.log(`stderr: ${data}`); });
|
||||
proc.on('error', error => { console.log(`error: ${error}`); });
|
||||
console.log("Waiting for creation of new map...");
|
||||
// create awaiter for fs.readFile
|
||||
const readFile = util.promisify(fs.readFile);
|
||||
while (true) {
|
||||
await this.sleep(1000);
|
||||
let fileData = await readFile(node.Name + '.log');
|
||||
if (fileData.includes('Instance created')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// start node
|
||||
proc = child_process.spawn("screen", ['-dmS', node.Name, '-L', '-Logfile', node.Name + '.log', 'node', 'client.js', 'start', node.Name]);
|
||||
proc.stdout.on('data', data => { console.log(`stdout: ${data}`); });
|
||||
proc.stderr.on('data', data => { console.log(`stderr: ${data}`); });
|
||||
proc.on('error', error => { console.log(`error: ${error}`); });
|
||||
}
|
||||
async CreateTeleportZonesForANodeInstance(nodeInstance) {
|
||||
await Helpers_1.asyncForEach(nodeInstance.TeleportZones.values(), async (zone) => {
|
||||
let o = zone.GenerateTeleportZoneInFactorioCoordinates();
|
||||
let cmd = `CreateZone('${o[0]}',${o[1]},${o[2]},${o[3]},${o[4]},${o[5]},true)`;
|
||||
await this.RunRconCommand(nodeInstance.ClusterioWorldId, `/c remote.call('trainTeleports','runCode',\"${cmd}\")`, cmd);
|
||||
});
|
||||
}
|
||||
async CreateTeleportRestrictionsForANodeInstance(nodeInstance) {
|
||||
await Helpers_1.asyncForEach(nodeInstance.TeleportZones.values(), async (zone) => {
|
||||
let o = zone.GenerateZoneRestriction();
|
||||
let cmd = `CreateZoneRestriction(${o[0]},'${o[1]}','${o[2]}')`;
|
||||
await this.RunRconCommand(nodeInstance.ClusterioWorldId, `/c remote.call('trainTeleports','runCode',\"${cmd}\")`, cmd);
|
||||
});
|
||||
}
|
||||
sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
exports.ClusterioMasterProxy = ClusterioMasterProxy;
|
||||
//# sourceMappingURL=ClusterioMasterProxy.js.map
|
||||
@@ -1,8 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
/** The multiplier used to convert from the grid's coordinate system to factorio's.
|
||||
* 1000 means a 1x1 grid node will have a factorio map size of 1000x1000. */
|
||||
exports.GridToFactorioCoordinateSystemMultiplier = 1000;
|
||||
/** The width of teleport zones when they get created. */
|
||||
exports.TeleportZoneWidth = 50;
|
||||
//# sourceMappingURL=Config.js.map
|
||||
@@ -1,177 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const typescript_collections_1 = require("typescript-collections");
|
||||
const Point_1 = require("./Point");
|
||||
const TeleportZone_1 = require("./TeleportZone");
|
||||
const TeleportZoneDirection_1 = require("./TeleportZoneDirection");
|
||||
class Grid {
|
||||
constructor() {
|
||||
this._nodes = new typescript_collections_1.Dictionary();
|
||||
this._minX = 0;
|
||||
this._minY = 0;
|
||||
this._maxX = 0;
|
||||
this._maxY = 0;
|
||||
}
|
||||
GetNodes() {
|
||||
return this._nodes.values();
|
||||
}
|
||||
/**
|
||||
* Adds a node to the grid
|
||||
* @param topLeftCoordinate The top left x,y coordinate of the node
|
||||
* @param nodeInstance
|
||||
*/
|
||||
AddNodeToGrid(nodeInstance) {
|
||||
let topLeftCoordinate = nodeInstance.TopLeftCoordinate;
|
||||
let pointsToSave = new Array();
|
||||
// check all points that make up nodeInstance for existing nodes
|
||||
for (let x = topLeftCoordinate.X; x < topLeftCoordinate.X + nodeInstance.Width; x++) {
|
||||
for (let y = topLeftCoordinate.Y; y < topLeftCoordinate.Y + nodeInstance.Height; y++) {
|
||||
let currentPoint = new Point_1.Point(x, y);
|
||||
let existingNode = this._nodes.getValue(currentPoint);
|
||||
if (existingNode != null) {
|
||||
throw new Error("Cannot add node to grid - collision detected with node '" + existingNode.Name + "'");
|
||||
}
|
||||
pointsToSave.push(currentPoint);
|
||||
}
|
||||
}
|
||||
// no existing nodes found to assign node to these points
|
||||
for (let i = 0; i < pointsToSave.length; i++) {
|
||||
this._nodes.setValue(pointsToSave[i], nodeInstance);
|
||||
}
|
||||
//TODO: Refactor
|
||||
//#region Set up teleport zones
|
||||
// check surrounding points for nodes
|
||||
// check north points
|
||||
let y = topLeftCoordinate.Y - 1;
|
||||
for (let x = topLeftCoordinate.X; x < topLeftCoordinate.X + nodeInstance.Width; x++) {
|
||||
let currentPoint = new Point_1.Point(x, y);
|
||||
let existingNode = this._nodes.getValue(currentPoint);
|
||||
// found a node - set up teleport zone
|
||||
if (existingNode != null) {
|
||||
// Create/Update teleport node in existing node to new node
|
||||
let teleportZoneFromExistingToNew = existingNode.TeleportZones.getValue(nodeInstance.Key);
|
||||
if (teleportZoneFromExistingToNew == null) {
|
||||
teleportZoneFromExistingToNew = new TeleportZone_1.TeleportZone(existingNode, TeleportZoneDirection_1.TeleportZoneDirection.South, currentPoint, currentPoint, existingNode.TeleportZones.size(), nodeInstance);
|
||||
existingNode.TeleportZones.setValue(nodeInstance.Key, teleportZoneFromExistingToNew);
|
||||
}
|
||||
else {
|
||||
teleportZoneFromExistingToNew.BottomRightCoordinate = currentPoint;
|
||||
}
|
||||
// Create/Update teleport node in new node to existing node
|
||||
let teleportZoneFromNewToExisting = nodeInstance.TeleportZones.getValue(existingNode.Key);
|
||||
let pointInNewNode = new Point_1.Point(currentPoint.X, currentPoint.Y + 1);
|
||||
if (teleportZoneFromNewToExisting == null) {
|
||||
teleportZoneFromNewToExisting = new TeleportZone_1.TeleportZone(nodeInstance, TeleportZoneDirection_1.TeleportZoneDirection.North, pointInNewNode, pointInNewNode, nodeInstance.TeleportZones.size(), existingNode);
|
||||
nodeInstance.TeleportZones.setValue(existingNode.Key, teleportZoneFromNewToExisting);
|
||||
}
|
||||
else {
|
||||
teleportZoneFromNewToExisting.BottomRightCoordinate = currentPoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
// check south points
|
||||
y = topLeftCoordinate.Y + nodeInstance.Height + 1;
|
||||
for (let x = topLeftCoordinate.X; x < topLeftCoordinate.X + nodeInstance.Width; x++) {
|
||||
let currentPoint = new Point_1.Point(x, y);
|
||||
let existingNode = this._nodes.getValue(currentPoint);
|
||||
// found a node - set up teleport zone
|
||||
if (existingNode != null) {
|
||||
// Create/Update teleport node in existing node to new node
|
||||
let teleportZoneFromExistingToNew = existingNode.TeleportZones.getValue(nodeInstance.Key);
|
||||
if (teleportZoneFromExistingToNew == null) {
|
||||
teleportZoneFromExistingToNew = new TeleportZone_1.TeleportZone(existingNode, TeleportZoneDirection_1.TeleportZoneDirection.North, currentPoint, currentPoint, existingNode.TeleportZones.size(), nodeInstance);
|
||||
existingNode.TeleportZones.setValue(nodeInstance.Key, teleportZoneFromExistingToNew);
|
||||
}
|
||||
else {
|
||||
teleportZoneFromExistingToNew.BottomRightCoordinate = currentPoint;
|
||||
}
|
||||
// Create/Update teleport node in new node to existing node
|
||||
let teleportZoneFromNewToExisting = nodeInstance.TeleportZones.getValue(existingNode.Key);
|
||||
let pointInNewNode = new Point_1.Point(currentPoint.X, currentPoint.Y - 1);
|
||||
if (teleportZoneFromNewToExisting == null) {
|
||||
teleportZoneFromNewToExisting = new TeleportZone_1.TeleportZone(nodeInstance, TeleportZoneDirection_1.TeleportZoneDirection.South, pointInNewNode, pointInNewNode, nodeInstance.TeleportZones.size(), existingNode);
|
||||
nodeInstance.TeleportZones.setValue(existingNode.Key, teleportZoneFromNewToExisting);
|
||||
}
|
||||
else {
|
||||
teleportZoneFromNewToExisting.BottomRightCoordinate = currentPoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
// check west points
|
||||
let x = topLeftCoordinate.X - 1;
|
||||
for (let y = topLeftCoordinate.Y; y < topLeftCoordinate.Y + nodeInstance.Height; y++) {
|
||||
let currentPoint = new Point_1.Point(x, y);
|
||||
let existingNode = this._nodes.getValue(currentPoint);
|
||||
// found a node - set up teleport zone
|
||||
if (existingNode != null) {
|
||||
// Create/Update teleport node in existing node to new node
|
||||
let teleportZoneFromExistingToNew = existingNode.TeleportZones.getValue(nodeInstance.Key);
|
||||
if (teleportZoneFromExistingToNew == null) {
|
||||
teleportZoneFromExistingToNew = new TeleportZone_1.TeleportZone(existingNode, TeleportZoneDirection_1.TeleportZoneDirection.East, currentPoint, currentPoint, existingNode.TeleportZones.size(), nodeInstance);
|
||||
existingNode.TeleportZones.setValue(nodeInstance.Key, teleportZoneFromExistingToNew);
|
||||
}
|
||||
else {
|
||||
teleportZoneFromExistingToNew.BottomRightCoordinate = currentPoint;
|
||||
}
|
||||
// Create/Update teleport node in new node to existing node
|
||||
let teleportZoneFromNewToExisting = nodeInstance.TeleportZones.getValue(existingNode.Key);
|
||||
let pointInNewNode = new Point_1.Point(currentPoint.X + 1, currentPoint.Y);
|
||||
if (teleportZoneFromNewToExisting == null) {
|
||||
teleportZoneFromNewToExisting = new TeleportZone_1.TeleportZone(nodeInstance, TeleportZoneDirection_1.TeleportZoneDirection.West, pointInNewNode, pointInNewNode, nodeInstance.TeleportZones.size(), existingNode);
|
||||
nodeInstance.TeleportZones.setValue(existingNode.Key, teleportZoneFromNewToExisting);
|
||||
}
|
||||
else {
|
||||
teleportZoneFromNewToExisting.BottomRightCoordinate = currentPoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
// check east points
|
||||
x = topLeftCoordinate.X + nodeInstance.Width + 1;
|
||||
for (let y = topLeftCoordinate.Y; y < topLeftCoordinate.Y + nodeInstance.Height; y++) {
|
||||
let currentPoint = new Point_1.Point(x, y);
|
||||
let existingNode = this._nodes.getValue(currentPoint);
|
||||
// found a node - set up teleport zone
|
||||
if (existingNode != null) {
|
||||
// Create/Update teleport node in existing node to new node
|
||||
let teleportZoneFromExistingToNew = existingNode.TeleportZones.getValue(nodeInstance.Key);
|
||||
if (teleportZoneFromExistingToNew == null) {
|
||||
teleportZoneFromExistingToNew = new TeleportZone_1.TeleportZone(existingNode, TeleportZoneDirection_1.TeleportZoneDirection.West, currentPoint, currentPoint, existingNode.TeleportZones.size(), nodeInstance);
|
||||
existingNode.TeleportZones.setValue(nodeInstance.Key, teleportZoneFromExistingToNew);
|
||||
}
|
||||
else {
|
||||
teleportZoneFromExistingToNew.BottomRightCoordinate = currentPoint;
|
||||
}
|
||||
// Create/Update teleport node in new node to existing node
|
||||
let teleportZoneFromNewToExisting = nodeInstance.TeleportZones.getValue(existingNode.Key);
|
||||
let pointInNewNode = new Point_1.Point(currentPoint.X - 1, currentPoint.Y);
|
||||
if (teleportZoneFromNewToExisting == null) {
|
||||
teleportZoneFromNewToExisting = new TeleportZone_1.TeleportZone(nodeInstance, TeleportZoneDirection_1.TeleportZoneDirection.East, pointInNewNode, pointInNewNode, nodeInstance.TeleportZones.size(), existingNode);
|
||||
nodeInstance.TeleportZones.setValue(existingNode.Key, teleportZoneFromNewToExisting);
|
||||
}
|
||||
else {
|
||||
teleportZoneFromNewToExisting.BottomRightCoordinate = currentPoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
this._nodes.setValue(topLeftCoordinate, nodeInstance);
|
||||
this._minX = Math.min(this._minX, topLeftCoordinate.X);
|
||||
this._minY = Math.min(this._minY, topLeftCoordinate.Y);
|
||||
this._maxX = Math.max(this._maxX, topLeftCoordinate.X + nodeInstance.Width);
|
||||
this._maxY = Math.max(this._maxY, topLeftCoordinate.Y + nodeInstance.Height);
|
||||
}
|
||||
get Width() {
|
||||
return this._maxX - this._minX;
|
||||
}
|
||||
get Height() {
|
||||
return this._maxY - this._minY;
|
||||
}
|
||||
get TopLeftCoordinate() {
|
||||
return new Point_1.Point(this._minX, this._minY);
|
||||
}
|
||||
get BottomRightCoordinate() {
|
||||
return new Point_1.Point(this._maxX, this._maxY);
|
||||
}
|
||||
}
|
||||
exports.Grid = Grid;
|
||||
//# sourceMappingURL=Grid.js.map
|
||||
@@ -1,10 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
//TODO: Make generic
|
||||
async function asyncForEach(array, callback) {
|
||||
for (let index = 0; index < array.length; index++) {
|
||||
await callback(array[index], index, array);
|
||||
}
|
||||
}
|
||||
exports.asyncForEach = asyncForEach;
|
||||
//# sourceMappingURL=Helpers.js.map
|
||||
@@ -1,3 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
//# sourceMappingURL=IApiSlaves.js.map
|
||||
@@ -1,3 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
//# sourceMappingURL=IClusterioMasterProxy.js.map
|
||||
@@ -1,27 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
console.log("started.");
|
||||
const ClusterioMasterProxy_1 = require("./ClusterioMasterProxy");
|
||||
const ClusterManager_1 = require("./ClusterManager");
|
||||
const argv = require('minimist')(process.argv.slice(2));
|
||||
let configPath = argv['configPath'];
|
||||
if (configPath === undefined || configPath === null || configPath === "") {
|
||||
throw new Error("configPath is null.");
|
||||
}
|
||||
;
|
||||
let config = require(configPath);
|
||||
let proxy = new ClusterioMasterProxy_1.ClusterioMasterProxy(config.masterIP, config.masterPort, config.masterAuthToken);
|
||||
let manager = new ClusterManager_1.ClusterManager();
|
||||
manager.GenerateBasicGridOfNodes(2, 2, 'GridLockTest_');
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
// async function test() {
|
||||
// await asyncForEach([1,2,3], async (v, i, a) => {
|
||||
// console.log("test" + i);
|
||||
// await sleep(5000);
|
||||
// });
|
||||
// }
|
||||
// test();
|
||||
proxy.CreateNodeInstancesOnLocalServer(manager.Grid.GetNodes());
|
||||
//# sourceMappingURL=ModelController.js.map
|
||||
@@ -1,24 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
console.log("started.");
|
||||
const ClusterioMasterProxy_1 = require("./ClusterioMasterProxy");
|
||||
const ClusterManager_1 = require("./ClusterManager");
|
||||
const argv = require('minimist')(process.argv.slice(2));
|
||||
let configPath = argv['configPath'];
|
||||
if (configPath === undefined || configPath === null || configPath === "") {
|
||||
throw new Error("configPath is null.");
|
||||
}
|
||||
;
|
||||
let config = require(configPath);
|
||||
let proxy = new ClusterioMasterProxy_1.ClusterioMasterProxy(config.masterIP, config.masterPort, config.masterAuthToken);
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
// main
|
||||
(async () => {
|
||||
let manager = new ClusterManager_1.ClusterManager();
|
||||
manager.GenerateBasicGridOfNodes(2, 2, 'GridLockTest');
|
||||
await proxy.CreateNodeInstancesOnLocalServer(manager.Grid.GetNodes());
|
||||
return;
|
||||
})();
|
||||
//# sourceMappingURL=ModelDemo.js.map
|
||||
@@ -1,61 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
console.log("Started.");
|
||||
const ClusterioMasterProxy_1 = require("./ClusterioMasterProxy");
|
||||
const ClusterManager_1 = require("./ClusterManager");
|
||||
const argv = require('minimist')(process.argv.slice(2));
|
||||
// arg: configPath
|
||||
// description: should point to the clusterio config.json file.
|
||||
let configPath = argv['configPath'];
|
||||
if (configPath === undefined || configPath === null || configPath === "") {
|
||||
configPath = "../../config.json";
|
||||
}
|
||||
;
|
||||
let config = require(configPath);
|
||||
// arg: mode
|
||||
// description: should be GenerateBasicGridOfNodes for now. Can extend in future.
|
||||
let mode = argv['mode'];
|
||||
if (mode === undefined || mode === null || mode === "") {
|
||||
throw new Error("mode is null. mode must be 'GenerateBasicGridOfNodes' (for now).");
|
||||
}
|
||||
;
|
||||
// arg: serviceHostType
|
||||
// description: what program to use to host the client.js instances.
|
||||
let serviceHostType = argv['serviceHostType'];
|
||||
if (serviceHostType === undefined || serviceHostType === null || serviceHostType === "") {
|
||||
throw new Error("serviceHostType is null. serviceHostType must be either 'pm2' or 'screen'.");
|
||||
}
|
||||
;
|
||||
if (serviceHostType != "pm2" && serviceHostType != "screen") {
|
||||
throw new Error("serviceHostType must be either 'pm2' or 'screen'.");
|
||||
}
|
||||
// arg: width
|
||||
// description: width of node grid
|
||||
let width = argv['width'];
|
||||
if (width === undefined || width === null || width === "") {
|
||||
throw new Error("width is null. width must be an int > 0.");
|
||||
}
|
||||
;
|
||||
// arg: height
|
||||
// description: height of node grid
|
||||
let height = argv['height'];
|
||||
if (height === undefined || height === null || height === "") {
|
||||
throw new Error("height is null. height must be an int > 0.");
|
||||
}
|
||||
;
|
||||
// arg: namePrefix
|
||||
// description: namePrefix of node grid
|
||||
let namePrefix = argv['namePrefix'];
|
||||
if (namePrefix === undefined || namePrefix === null || namePrefix === "") {
|
||||
throw new Error("namePrefix is null.");
|
||||
}
|
||||
;
|
||||
let proxy = new ClusterioMasterProxy_1.ClusterioMasterProxy(config.masterIP, config.masterPort, config.masterAuthToken, serviceHostType);
|
||||
// main
|
||||
(async () => {
|
||||
let manager = new ClusterManager_1.ClusterManager();
|
||||
manager.GenerateBasicGridOfNodes(width, height, namePrefix);
|
||||
await proxy.CreateNodeInstancesOnLocalServer(manager.Grid.GetNodes());
|
||||
return;
|
||||
})();
|
||||
//# sourceMappingURL=ModelRunner.js.map
|
||||
@@ -1,29 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const Point_1 = require("./Point");
|
||||
const typescript_collections_1 = require("typescript-collections");
|
||||
/**
|
||||
* An instance of a Factorio server. Multiple nodes can run on the same server.
|
||||
*/
|
||||
class NodeInstance {
|
||||
constructor(name, topLeftCoordinate, width, height) {
|
||||
this.TeleportZones = new typescript_collections_1.Dictionary();
|
||||
this.Name = name;
|
||||
this.Width = width;
|
||||
this.Height = height;
|
||||
this.TopLeftCoordinate = topLeftCoordinate;
|
||||
}
|
||||
get BottomRightCoordinate() {
|
||||
return new Point_1.Point(this.TopLeftCoordinate.X + this.Width, this.TopLeftCoordinate.Y + this.Height);
|
||||
}
|
||||
// public AddTeleportZone(name: string, topLeftCoordinate: Point, bottomRightCoordinate: Point) {
|
||||
// this.TeleportZones.push(new TeleportZone(this, topLeftCoordinate, bottomRightCoordinate, name));
|
||||
// }
|
||||
AddTeleportZonePoint() {
|
||||
}
|
||||
get Key() {
|
||||
return this.Name;
|
||||
}
|
||||
}
|
||||
exports.NodeInstance = NodeInstance;
|
||||
//# sourceMappingURL=NodeInstance.js.map
|
||||
@@ -1,57 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
class Point {
|
||||
/**
|
||||
* Constructs a Point. Will not accept NaNs.
|
||||
* @param x A number. Cannot be NaN.
|
||||
* @param y A number. Cannot be NaN.
|
||||
*/
|
||||
constructor(x, y) {
|
||||
this.toString = () => {
|
||||
return "(" + this.X + "," + this.Y + ")";
|
||||
};
|
||||
if (Number.isNaN(x)) {
|
||||
throw new Error("x is NaN. Will not construct Point.");
|
||||
}
|
||||
if (Number.isNaN(y)) {
|
||||
throw new Error("y is NaN. Will not construct Point.");
|
||||
}
|
||||
this.X = x;
|
||||
this.Y = y;
|
||||
}
|
||||
/**
|
||||
* Constructs a Point from a string representation in the format (x,y). Will not accept NaNs.
|
||||
* @param vectorAsString A string in the format (x,y). Will not accept NaNs.
|
||||
*/
|
||||
static FromString(vectorAsString) {
|
||||
this.CheckChar('(', vectorAsString[0], vectorAsString);
|
||||
this.CheckChar(')', vectorAsString[vectorAsString.length - 1], vectorAsString);
|
||||
if (vectorAsString.indexOf(',') === -1) {
|
||||
throw new Error("vectorAsString is an invalid format. Cannot find ','. fullString: " + vectorAsString);
|
||||
}
|
||||
let splitStr = vectorAsString.substr(1, vectorAsString.length - 2).split(',');
|
||||
if (splitStr.length != 2) {
|
||||
throw new Error("vectorAsString is an invalid format. Expecting string in format (x,y). fullString: " + vectorAsString);
|
||||
}
|
||||
let x = parseInt(splitStr[0]);
|
||||
let y = parseInt(splitStr[1]);
|
||||
let v = new Point(x, y);
|
||||
return v;
|
||||
}
|
||||
static CheckChar(expectedChar, actualChar, fullString) {
|
||||
if (actualChar != expectedChar) {
|
||||
throw new Error("vectorAsString is an invalid format. Expecting: '" + expectedChar + "'. Actual: '" + actualChar + "'. fullString: " + fullString);
|
||||
}
|
||||
}
|
||||
equals(obj) {
|
||||
return this.X === obj.X && this.Y === obj.Y;
|
||||
}
|
||||
Add(p) {
|
||||
return new Point(this.X + p.X, this.Y + p.Y);
|
||||
}
|
||||
Subtract(p) {
|
||||
return new Point(this.X - p.X, this.Y - p.Y);
|
||||
}
|
||||
}
|
||||
exports.Point = Point;
|
||||
//# sourceMappingURL=Point.js.map
|
||||
@@ -1,16 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
/**
|
||||
* A server. Can contain multiple nodes (Factorio server instances).
|
||||
*/
|
||||
class Server {
|
||||
constructor(name) {
|
||||
this.Nodes = new Array();
|
||||
this.Name = name;
|
||||
}
|
||||
AddNodeInstance(node) {
|
||||
this.Nodes.push(node);
|
||||
}
|
||||
}
|
||||
exports.Server = Server;
|
||||
//# sourceMappingURL=Server.js.map
|
||||
@@ -1,8 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var ServiceHostType;
|
||||
(function (ServiceHostType) {
|
||||
ServiceHostType[ServiceHostType["pm2"] = 0] = "pm2";
|
||||
ServiceHostType[ServiceHostType["screen"] = 1] = "screen";
|
||||
})(ServiceHostType = exports.ServiceHostType || (exports.ServiceHostType = {}));
|
||||
//# sourceMappingURL=ServiceHostType.js.map
|
||||
@@ -1,64 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const Point_1 = require("./Point");
|
||||
const TeleportZoneDirection_1 = require("./TeleportZoneDirection");
|
||||
const Config = require("./Config");
|
||||
class TeleportZone {
|
||||
constructor(parentNode, direction, topLeftCoordinate, bottomRightCoordinate, index, targetNode) {
|
||||
this.TopLeftCoordinate = topLeftCoordinate;
|
||||
this.BottomRightCoordinate = bottomRightCoordinate;
|
||||
this.ParentNode = parentNode;
|
||||
this.Direction = direction;
|
||||
this.Index = index;
|
||||
this.TargetNode = targetNode;
|
||||
}
|
||||
get Name() {
|
||||
return TeleportZoneDirection_1.TeleportZoneDirection[this.Direction] + " to " + this.TargetNode.Name;
|
||||
}
|
||||
// public AddRestriction(restrictingTeleportZone: TeleportZone) {
|
||||
// this.Restrictions.push(new TeleportZoneRestriction(this, restrictingTeleportZone));
|
||||
// }
|
||||
/**
|
||||
* Converts this teleport zone into Factorio map co-ordinates
|
||||
* @returns [name, topleft.x, topleft.y, width, height, zoneIndex]
|
||||
*/
|
||||
GenerateTeleportZoneInFactorioCoordinates() {
|
||||
// get where this zone is relative to the node itself
|
||||
let topLeftRelativeCoordinate = this.TopLeftCoordinate.Subtract(this.ParentNode.TopLeftCoordinate);
|
||||
// a grid coordinate point is equivalent to a 1x1 node, so bottomright = topleft.
|
||||
// add (1,1) to translate from area representation to coordinates representing actual points.
|
||||
let bottomRightRelativeCoordinate = this.BottomRightCoordinate
|
||||
.Subtract(this.ParentNode.TopLeftCoordinate)
|
||||
.Add(new Point_1.Point(1, 1));
|
||||
// Factorio map is e.g. (-500,-500) to (500, 500) for a 1000x1000 node.
|
||||
// We therefore need to offset our co-ordinates so that (0,0) is the center and not (width/2, height/2)
|
||||
let m = Config.GridToFactorioCoordinateSystemMultiplier;
|
||||
let factorioTeleportZoneTopLeft = new Point_1.Point(topLeftRelativeCoordinate.X * m - m / 2, topLeftRelativeCoordinate.Y * m - m / 2);
|
||||
let factorioTeleportZoneBottomRight = new Point_1.Point(bottomRightRelativeCoordinate.X * m - m / 2, bottomRightRelativeCoordinate.Y * m - m / 2);
|
||||
let zoneWidth = factorioTeleportZoneBottomRight.X - factorioTeleportZoneTopLeft.X;
|
||||
let zoneHeight = factorioTeleportZoneBottomRight.Y - factorioTeleportZoneTopLeft.Y;
|
||||
let tl = factorioTeleportZoneTopLeft;
|
||||
let br = factorioTeleportZoneBottomRight;
|
||||
let zw = Config.TeleportZoneWidth;
|
||||
switch (this.Direction) {
|
||||
case TeleportZoneDirection_1.TeleportZoneDirection.North:
|
||||
return [this.Name, tl.X, tl.Y, zoneWidth, zw, this.Index + 1];
|
||||
case TeleportZoneDirection_1.TeleportZoneDirection.West:
|
||||
return [this.Name, tl.X, tl.Y, zw, zoneHeight, this.Index + 1];
|
||||
case TeleportZoneDirection_1.TeleportZoneDirection.South:
|
||||
return [this.Name, tl.X, br.Y - zw, zoneWidth, zw, this.Index + 1];
|
||||
case TeleportZoneDirection_1.TeleportZoneDirection.East:
|
||||
return [this.Name, br.X - zw, tl.Y, zw, zoneHeight, this.Index + 1];
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Generates a zone restriction from this teleport zone to target zone
|
||||
* @returns [zoneIndex, targetNodeName, targetZoneName]
|
||||
*/
|
||||
GenerateZoneRestriction() {
|
||||
let targetZone = this.TargetNode.TeleportZones.getValue(this.ParentNode.Key);
|
||||
return [this.Index + 1, this.TargetNode.Name, targetZone.Name];
|
||||
}
|
||||
}
|
||||
exports.TeleportZone = TeleportZone;
|
||||
//# sourceMappingURL=TeleportZone.js.map
|
||||
@@ -1,10 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var TeleportZoneDirection;
|
||||
(function (TeleportZoneDirection) {
|
||||
TeleportZoneDirection[TeleportZoneDirection["North"] = 0] = "North";
|
||||
TeleportZoneDirection[TeleportZoneDirection["East"] = 1] = "East";
|
||||
TeleportZoneDirection[TeleportZoneDirection["South"] = 2] = "South";
|
||||
TeleportZoneDirection[TeleportZoneDirection["West"] = 3] = "West";
|
||||
})(TeleportZoneDirection = exports.TeleportZoneDirection || (exports.TeleportZoneDirection = {}));
|
||||
//# sourceMappingURL=TeleportZoneDirection.js.map
|
||||
@@ -1,10 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
class TeleportZoneRestriction {
|
||||
constructor(parentTeleportZone, restrictingTeleportZone) {
|
||||
this.ParentTeleportZone = parentTeleportZone;
|
||||
this.RestrictingTeleportZone = restrictingTeleportZone;
|
||||
}
|
||||
}
|
||||
exports.TeleportZoneRestriction = TeleportZoneRestriction;
|
||||
//# sourceMappingURL=TeleportZoneRestriction.js.map
|
||||
@@ -1,13 +0,0 @@
|
||||
"use strict";
|
||||
function __export(m) {
|
||||
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
|
||||
}
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
__export(require("./ClusterManager"));
|
||||
__export(require("./NodeInstance"));
|
||||
__export(require("./Server"));
|
||||
__export(require("./TeleportZone"));
|
||||
__export(require("./TeleportZoneRestriction"));
|
||||
__export(require("./Point"));
|
||||
__export(require("./Grid"));
|
||||
//# sourceMappingURL=index.js.map
|
||||
@@ -1,28 +0,0 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
// const config = require("./../config");
|
||||
|
||||
module.exports = function(config){
|
||||
return {
|
||||
middleware: function (req, res, next) {
|
||||
var token = req.headers['x-access-token'];
|
||||
if(!token) return res.status(401).send({ auth: false, message: 'No token provided.', endpoint: req.route.path});
|
||||
|
||||
jwt.verify(token, config.masterAuthSecret, function(err, decoded) {
|
||||
if(err) return res.status(500).send({ auth: false, message: 'Failed to authenticate token.', endpoint: req.route.path});
|
||||
next();
|
||||
});
|
||||
},
|
||||
check: async function(token){
|
||||
return new Promise((resolve, reject) => {
|
||||
jwt.verify(token, config.masterAuthSecret, function(err, decoded) {
|
||||
if(err){
|
||||
resolve({ok:false, msg:err})
|
||||
} else {
|
||||
resolve({ok:true, msg:"Successfully authenticated"});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
/**
|
||||
|
||||
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"),
|
||||
})
|
||||
@@ -1,119 +0,0 @@
|
||||
//Sigmoids...
|
||||
|
||||
function sigmoid0(x)
|
||||
{
|
||||
return x/(1+Math.abs(x));
|
||||
}
|
||||
function sigmoid1(x)
|
||||
{
|
||||
if (x>=0.0) {
|
||||
return 1.0
|
||||
} else {
|
||||
return 0.0
|
||||
}
|
||||
}
|
||||
function sigmoid2(x)
|
||||
{
|
||||
if (x>=0.0){
|
||||
return x
|
||||
} else {
|
||||
return 0.0
|
||||
}
|
||||
}
|
||||
function sigmoid3(x)
|
||||
{
|
||||
return x;
|
||||
}
|
||||
|
||||
//DOSE - Doses items for item X slave Y
|
||||
//Numreq - number of items that are requested for item X for slave Y
|
||||
//Instore - number of items for item X in master gauge (currently)
|
||||
//store_last_tick - Number of items for item X in master gauge at time of last tick
|
||||
//dole - Current dole for item X
|
||||
//carry - carry for item X slave Y
|
||||
//prev_req - Numreq for previous request for item X slave Y
|
||||
//* doleinfo_last - Object with 2 values for last "tick period": numreq - total number or requests for item, numslave - number of slaves requesting that item
|
||||
//debt - debt for item X slave Y
|
||||
|
||||
//Returns: Array of 3 elements
|
||||
//0.Number of items to give in that dose
|
||||
//1.New dole for item X
|
||||
//2.New carry for item X slave Y
|
||||
//3.New debt
|
||||
function Dose(numreq,instore,store_last_tick,dole,carry,prev_req,numreq_total_adj,debt)
|
||||
{
|
||||
numreq=Number(numreq);
|
||||
instore=Number(instore);
|
||||
var instore_adj=instore;
|
||||
if (instore_adj==0) instore_adj=0.1;
|
||||
|
||||
var b=Math.trunc(prev_req/instore_adj*numreq);
|
||||
var inp1=[numreq,instore_adj,store_last_tick/numreq,dole,carry];//0..4
|
||||
var inp2=[prev_req/numreq,numreq/numreq_total_adj,store_last_tick/numreq_total_adj,prev_req/instore_adj,b,numreq_total_adj,store_last_tick,debt];//5..12
|
||||
var inputs=inp1.concat(inp2);
|
||||
var internal=new Array(20).fill(0);
|
||||
var outputs=[0,0,0,0];
|
||||
//NN: Please don't touch :), code is generated
|
||||
|
||||
outputs[0]=inputs[7]*0.8058582544;
|
||||
outputs[0]=sigmoid2(outputs[0]);
|
||||
outputs[1]=sigmoid0(outputs[1]);
|
||||
outputs[2]=sigmoid0(outputs[2]);
|
||||
outputs[3]=inputs[6]*0.2294657379;
|
||||
outputs[3]=sigmoid0(outputs[3]);
|
||||
|
||||
debt=debt || 0;
|
||||
if (outputs[0]>0.99) outputs[0]=1.0;
|
||||
|
||||
var res1=Number(outputs[0])*numreq+outputs[3]+debt;
|
||||
var res_capped=Math.round(res1);
|
||||
|
||||
if (res_capped>numreq) res_capped=numreq;
|
||||
if (res_capped>instore) res_capped=instore;
|
||||
if (res_capped<0) res_capped=0;
|
||||
|
||||
res1=res1 || 0;
|
||||
res_capped=res_capped || 0;//Safeguard against wrong values
|
||||
debt=res1-res_capped;
|
||||
debt_cap=Math.round(numreq*1.5)+5;
|
||||
if (debt>debt_cap) debt=debt_cap;
|
||||
|
||||
carry=sigmoid0(outputs[2]*carry+outputs[0]) || 0;
|
||||
return [res_capped,dole+outputs[1],carry,debt];
|
||||
}
|
||||
|
||||
//TICK - Updates values for item X, should be called once for each item for each round of all slaves requesting that item
|
||||
//Instore - number of items for item X in master gauge (currently)
|
||||
//dole - Current dole for item X
|
||||
//store_last_tick - Number of items for item X in master gauge at time of last tick (not this one)
|
||||
//* doleinfo_last - Object with 2 values for last "tick period": numreq - total number or requests for item, numslave - number of slaves requesting that item
|
||||
//* doleinfo_new - Object with 2 values for new "tick period": numreq - total number or requests for item, numslave - number of slaves requesting that item
|
||||
|
||||
|
||||
//Returns: Array of 2 elements
|
||||
//0.New dole for item X
|
||||
//1.Guessed percentage average of items supplied in last 10 ticks
|
||||
function Tick(instore,dole,store_last_tick,numreq_total_adj)
|
||||
{
|
||||
var instore_adj=instore;
|
||||
if (instore_adj==0) instore_adj=0.1;
|
||||
var inputs=[instore,dole,store_last_tick/instore_adj,store_last_tick/numreq_total_adj,store_last_tick,numreq_total_adj];
|
||||
var internal=new Array(10).fill(0);
|
||||
var outputs=[0,0];
|
||||
//NN: Please don't touch :), code is generated
|
||||
|
||||
outputs[0]=inputs[3]*0.9364774823;
|
||||
outputs[0]=outputs[0]*1.0*0.4590721726;
|
||||
outputs[0]=sigmoid2(outputs[0]);
|
||||
outputs[1]=outputs[0]*1.828422785;
|
||||
outputs[1]=sigmoid3(outputs[1]);
|
||||
|
||||
avg=outputs[1];
|
||||
if (numreq_total_adj<0.11) avg=1;//When no item's are requested, result is 0.1 ; In that case there is no deficit, so return 1 = 100% demand fulfilled
|
||||
if (avg>1) avg=1;
|
||||
if (avg<0) avg=0;
|
||||
|
||||
return [sigmoid0(outputs[0]),avg];
|
||||
}
|
||||
|
||||
module.exports = {Dose,Tick}
|
||||
@@ -1,167 +0,0 @@
|
||||
/**
|
||||
|
||||
class factorioServer
|
||||
|
||||
This class has the responsiblity of :
|
||||
|
||||
* spawning a factorio server
|
||||
* Handling input via stdin or RCON
|
||||
|
||||
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
const child_process = require("child_process");
|
||||
const path = require("path");
|
||||
|
||||
const mkdirp = require("mkdirp");
|
||||
|
||||
const fileOps = require("lib/fileOps");
|
||||
|
||||
module.exports = class factorioServer {
|
||||
constructor({
|
||||
factorioDirectory = "factorio",
|
||||
instanceDirectory = "instances/data",
|
||||
instanceName = "Default server name",
|
||||
factorioPort = Math.floor(Math.random() * 65535),
|
||||
serversettings = {},
|
||||
onProgress = function(type, a, b, c){
|
||||
if(type == "progress") console.log("Progress: ",a," of ",b," unit: ",c);
|
||||
if(type == "log") console.log(a);
|
||||
},
|
||||
} = {}){
|
||||
this.factorioDirectory = factorioDirectory;
|
||||
this.instanceDirectory = instanceDirectory;
|
||||
this.instanceName = instanceName;
|
||||
this.factorioPort = factorioPort;
|
||||
this.serversettings = Object.assign({
|
||||
"name": "Clusterio instance: " + this.instanceName,
|
||||
"description": "Public factorio server running clusterio",
|
||||
"tags": ["clusterio"],
|
||||
"max_players": "20",
|
||||
"visibility": "public",
|
||||
"username": "",
|
||||
"token": "",
|
||||
"game_password": "",
|
||||
"verify_user_identity": true,
|
||||
"admins": ["Danielv123"],
|
||||
"allow_commands": true,
|
||||
"autosave_interval": 10,
|
||||
"autosave_slots": 5,
|
||||
"afk_autokick_interval": 0,
|
||||
"auto_pause": false,
|
||||
}, serversettings);
|
||||
this.onProgress = (type, a, b, c) => {
|
||||
if(typeof onProgress == "function") onProgress(type, a, b, c);
|
||||
};
|
||||
}
|
||||
async initialize(){
|
||||
if(!await this.isValidInstance()){
|
||||
await this.createInstance();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
async createInstance(path = this.instanceDirectory){
|
||||
if(await this.isValidInstance(path)){
|
||||
throw new Error("Instance already exists!");
|
||||
}
|
||||
|
||||
// create folders
|
||||
let folders = [
|
||||
"/script-output/",
|
||||
"/saves/",
|
||||
"/mods/",
|
||||
"/instanceMods/",
|
||||
];
|
||||
mkdirp.sync(path);
|
||||
folders.forEach(folder => mkdirp.sync(path + folder));
|
||||
|
||||
// create files
|
||||
let files = [
|
||||
{name: "/script-output/output.txt", content: ""},
|
||||
{name: "/script-output/orders.txt", content: ""},
|
||||
{name: "/script-output/txbuffer.txt", content: ""},
|
||||
{name: "/config.ini", content: "[path]\r\n\ read-data=__PATH__executable__/../../data\r\n\ write-data=__PATH__executable__/../../../" + this.instanceDirectory + "\r\n\ "},
|
||||
{name: "/config.json", content: JSON.stringify({
|
||||
factorioPort: this.factorioPort,
|
||||
clientPassword: Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 8),
|
||||
}, null, 4)},
|
||||
{name: "/server-settings.json", content: JSON.stringify(this.serversettings, null, 4)},
|
||||
];
|
||||
files.forEach(file => {
|
||||
fs.writeFile(path + file.name, file.content, doneWritingHandler);
|
||||
// console.log("Created: ",path+file.name);
|
||||
});
|
||||
|
||||
let filesToWrite = files.length;
|
||||
function doneWritingHandler(){
|
||||
if(!--filesToWrite){
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
async deleteInstance(path = this.instanceDirectory){
|
||||
if(await this.isValidInstance(path)){
|
||||
await fileOps.deleteFolderRecursive(path);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
isValidInstance(path = this.instanceDirectory){
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.stat(path, (err, stat) => {
|
||||
if(err){
|
||||
// console.log(err)
|
||||
resolve(false);
|
||||
} else {
|
||||
// the instance folder exists, do some more checks
|
||||
fs.readdir(path, (err, files) => {
|
||||
if(files && Array.isArray(files)){
|
||||
let foldersThatShouldBeThere = [
|
||||
"script-output",
|
||||
"saves",
|
||||
"mods",
|
||||
"instanceMods",
|
||||
];
|
||||
let instanceValidity = true;
|
||||
foldersThatShouldBeThere.forEach(folder => {
|
||||
if(!files.includes(folder)){
|
||||
instanceValidity = false;
|
||||
}
|
||||
});
|
||||
resolve(instanceValidity);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
createMap(name = "map.zip"){
|
||||
return new Promise((resolve, reject) => {
|
||||
this.isValidInstance().then(isValid => {
|
||||
if(!isValid){
|
||||
return createInstance();
|
||||
}
|
||||
}).then(()=>{
|
||||
this.onProgress("log", "Starting map creation");
|
||||
// console.log(this.factorioDirectory + '/bin/x64/factorio')
|
||||
let proc = child_process.spawn(
|
||||
path.join(this.factorioDirectory, 'bin', 'x64', 'factorio'), [
|
||||
'-c', path.join(this.instanceDirectory,'config.ini'),
|
||||
'--create', path.join(this.instanceDirectory, 'saves',name),
|
||||
'--server-settings', path.join(this.instanceDirectory, 'server-settings.json'),
|
||||
],{
|
||||
cwd:process.cwd(),
|
||||
}
|
||||
);
|
||||
// console.log(proc.stdout.toString("utf8"));
|
||||
proc.stdout.on("data", data => this.onProgress("log", data.toString("utf8")));
|
||||
proc.on("close", code => {
|
||||
this.onProgress("log", "Map created! "+code);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const parallel = require("mocha.parallel");
|
||||
const mkdirp = require("mkdirp");
|
||||
|
||||
const factorioServer = require("./index.js");
|
||||
|
||||
describe("class factorioServer/index.js", ()=>{
|
||||
it("is a class (or at least a function)", ()=>{
|
||||
assert.equal(typeof factorioServer, "function");
|
||||
});
|
||||
describe(".deleteInstance(instanceName)", ()=>{
|
||||
it("deletes an instance, defaulting to the classes instanceDirectory", async () => {
|
||||
let server = new factorioServer({
|
||||
instanceDirectory: path.resolve(__dirname, "_testData_deleteInstance"),
|
||||
instanceName: "Unit testing instance",
|
||||
});
|
||||
await server.initialize();
|
||||
await server.deleteInstance();
|
||||
assert.equal(await server.isValidInstance(), false);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
describe(".isValidInstance(instanceName)", ()=>{
|
||||
it("tells us whether an instance exists or not", async () => {
|
||||
let server = new factorioServer({
|
||||
instanceDirectory: path.join(__dirname, "_testData_isValidInstance"),
|
||||
instanceName: "Unit testing instance",
|
||||
});
|
||||
await server.deleteInstance();
|
||||
let status = await server.isValidInstance();
|
||||
assert.equal(status, false);
|
||||
let x = await server.initialize(); // creates our instance
|
||||
let status2 = await server.isValidInstance();
|
||||
assert.equal(status2, true);
|
||||
|
||||
await server.deleteInstance(); // clean up folder
|
||||
return true;
|
||||
});
|
||||
it("does not get tricked by an empty folder", async () => {
|
||||
let server = new factorioServer({
|
||||
instanceDirectory: path.join(__dirname, "_testData_isValidInstance_2"),
|
||||
instanceName: "Unit testing instance",
|
||||
});
|
||||
assert.equal(await server.isValidInstance(), false);
|
||||
|
||||
mkdirp.sync(path.join(__dirname, "_testData_isValidInstance_2")); // create empty folder simulating instance
|
||||
assert.equal(await server.isValidInstance(), false);
|
||||
|
||||
await server.initialize(); // create a proper instance
|
||||
assert.equal(await server.isValidInstance(), true);
|
||||
|
||||
await server.deleteInstance(); // delete the instance and ensure we see that it is in fact deleted
|
||||
assert.equal(await server.isValidInstance(), false);
|
||||
|
||||
return true;
|
||||
}).timeout(5000);
|
||||
});
|
||||
describe(".initialize()", ()=>{
|
||||
it("creates an instance", done => {
|
||||
let server = new factorioServer({
|
||||
instanceDirectory: path.resolve(__dirname, "_testData_initialize"),
|
||||
instanceName: "Unit testing instance",
|
||||
});
|
||||
server.initialize().then(()=>{
|
||||
return server.deleteInstance();
|
||||
}).then(() => {
|
||||
done();
|
||||
}).catch(e => console.log(e));
|
||||
});
|
||||
});
|
||||
describe(".createMap(mapName.zip)", ()=>{
|
||||
it("creates a map file as instanceDirectory/saves/map.zip", async function(){
|
||||
this.timeout(20000); // these test often takes a lot of time in the map creation step (4500 ms on i3 dev system)
|
||||
let relativePath = path.relative('.', __dirname);
|
||||
let server = new factorioServer({
|
||||
instanceDirectory: path.join(relativePath, "_testData_createMap"),
|
||||
instanceName: "Unit testing instance",
|
||||
onProgress: false,
|
||||
});
|
||||
await server.initialize();
|
||||
let maps = fs.readdirSync(path.join(relativePath, "_testData_createMap", "saves"));
|
||||
assert.equal(maps.length, 0);
|
||||
await server.createMap();
|
||||
let maps2 = fs.readdirSync(path.join(relativePath, "_testData_createMap", "saves"));
|
||||
assert(maps2.includes("map.zip"));
|
||||
|
||||
await server.deleteInstance();
|
||||
return true;
|
||||
});
|
||||
it("supports custom filenames", async function(){
|
||||
this.timeout(20000);
|
||||
let relativePath = path.relative('.', __dirname);
|
||||
let server = new factorioServer({
|
||||
instanceDirectory: path.join(relativePath, "_testData_createMap_2"),
|
||||
instanceName: "Unit testing instance",
|
||||
onProgress: false,
|
||||
});
|
||||
await server.initialize();
|
||||
|
||||
let maps = fs.readdirSync(path.join(relativePath, "_testData_createMap_2", "saves"));
|
||||
assert.equal(maps.length, 0);
|
||||
await server.createMap("mapWithCustomName.zip");
|
||||
let maps2 = fs.readdirSync(path.join(relativePath, "_testData_createMap_2", "saves"));
|
||||
assert(maps2.includes("mapWithCustomName.zip"));
|
||||
|
||||
await server.deleteInstance();
|
||||
return true;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,120 +0,0 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
// gets newest file in a directory
|
||||
// dir is directory
|
||||
// files is array of filenames
|
||||
// callback(err, filename string);
|
||||
var getNewestFile = function(dir, files, callback) {
|
||||
if (!callback) throw new Error("Missing callback");
|
||||
if (!files || (files && files.length === 0)) {
|
||||
callback(new Error("No files"));
|
||||
return;
|
||||
}
|
||||
// we are appending the file name to the directory, without the
|
||||
// "/" we would get paths like /folderfile.txt instead of /folder/file.txt
|
||||
if(dir[dir.length-1] != "/"){
|
||||
dir += "/";
|
||||
}
|
||||
var newest = { file: files[0] };
|
||||
var checked = 0;
|
||||
fs.stat(dir + newest.file, function(err, stats) {
|
||||
if(err) {
|
||||
callback(err);
|
||||
} else {
|
||||
newest.mtime = stats.mtime;
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
var file = files[i];
|
||||
(function(file) {
|
||||
fs.stat(file, function(err, stats) {
|
||||
++checked;
|
||||
if (stats.mtime.getTime() > newest.mtime.getTime()) {
|
||||
newest = { file : file, mtime : stats.mtime };
|
||||
}
|
||||
if (checked == files.length) {
|
||||
callback(undefined,newest);
|
||||
}
|
||||
});
|
||||
})(dir + file);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// get names of all directories in directory
|
||||
var getDirectoriesSync = function(srcpath) {
|
||||
return fs.readdirSync(srcpath).filter(function (file) {
|
||||
return fs.statSync(path.join(srcpath, file)).isDirectory();
|
||||
});
|
||||
}
|
||||
// get names of all files in a directory
|
||||
var getFileNamesSync = function(srcpath) {
|
||||
return fs.readdirSync(srcpath).filter(function (file) {
|
||||
return !fs.statSync(path.join(srcpath, file)).isDirectory();
|
||||
});
|
||||
}
|
||||
// does what it says on the tin
|
||||
var deleteFolderRecursiveSync = function(path) {
|
||||
if (fs.existsSync(path)) {
|
||||
fs.readdirSync(path).forEach(function (file, index) {
|
||||
var curPath = path + "/" + file;
|
||||
if (fs.lstatSync(curPath).isDirectory()) { // recurse
|
||||
deleteFolderRecursiveSync(curPath);
|
||||
} else { // delete file
|
||||
fs.unlinkSync(curPath);
|
||||
}
|
||||
});
|
||||
fs.rmdirSync(path);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
var deleteFolderRecursive = function(path){
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readdir(path, (err, files) => {
|
||||
if(err){
|
||||
reject(err);
|
||||
} else {
|
||||
let thingsToDelete = files.length;
|
||||
files.forEach(file => {
|
||||
let curPath = path + "/" + file;
|
||||
fs.stat(curPath, (err, stat) => {
|
||||
if(!err){
|
||||
if(stat.isDirectory()){
|
||||
// recurse
|
||||
deleteFolderRecursive(curPath).then(() => {
|
||||
whenFilesDeleted();
|
||||
}).catch(e => console.log(e));
|
||||
} else {
|
||||
fs.unlink(curPath, () => {
|
||||
// console.log("Deleted "+curPath);
|
||||
whenFilesDeleted();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
whenFilesDeleted();
|
||||
function whenFilesDeleted(){
|
||||
if(!thingsToDelete--){// var-- instead of --var because we are calling the function once too much and need the delay
|
||||
fs.rmdir(path, () => {
|
||||
resolve();
|
||||
// console.log("Deleted "+path);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getNewestFile,
|
||||
getDirectoriesSync,
|
||||
deleteFolderRecursiveSync,
|
||||
deleteFolderRecursive,
|
||||
getFileNamesSync,
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const fileOps = require('./fileOps');
|
||||
|
||||
// create folders for testing
|
||||
function setupTestingEnv(){
|
||||
if (!fs.existsSync(path.resolve(__dirname, "database"))){
|
||||
fs.mkdirSync(path.resolve(__dirname, "database"));
|
||||
}
|
||||
if (!fs.existsSync(path.resolve(__dirname, "test"))){
|
||||
fs.mkdirSync(path.resolve(__dirname, "test"));
|
||||
}
|
||||
if (!fs.existsSync(path.resolve(__dirname, "test/folder"))){
|
||||
fs.mkdirSync(path.resolve(__dirname, "test/folder"));
|
||||
}
|
||||
if (!fs.existsSync(path.resolve(__dirname, "test/another folder"))){
|
||||
fs.mkdirSync(path.resolve(__dirname, "test/another folder"));
|
||||
}
|
||||
if (!fs.existsSync(path.resolve(__dirname, "test/file.txt"))){
|
||||
fs.writeFileSync(path.resolve(__dirname, "test/file.txt"), "contents");
|
||||
}
|
||||
if (!fs.existsSync(path.resolve(__dirname, "test/another file.txt"))){
|
||||
fs.writeFileSync(path.resolve(__dirname, "test/another file.txt"), "more contents");
|
||||
}
|
||||
// we need this xfile to test a branch in fileOps.getNewestFile()s propagation
|
||||
if (!fs.existsSync(path.resolve(__dirname, "test/xfile.txt"))){
|
||||
fs.writeFileSync(path.resolve(__dirname, "test/xfile.txt"), "contents");
|
||||
}
|
||||
}
|
||||
setupTestingEnv();
|
||||
setupTestingEnv(); // run twice to get branch coverage
|
||||
describe("fileOps.js", function(){
|
||||
describe("fileOps.getDirectoriesSync()", function(){
|
||||
it("gets an array of directory names from a path", function(){
|
||||
let directories = fileOps.getDirectoriesSync(path.resolve(__dirname, "test"));
|
||||
|
||||
// because there are no arrays for some reason
|
||||
assert(typeof directories == "object");
|
||||
|
||||
// they are sorted alphabetically because thats the way it is
|
||||
assert(directories[0] == "another folder" && directories[1] == "folder");
|
||||
});
|
||||
it("does not return any filenames", function(){
|
||||
let directories = fileOps.getDirectoriesSync(path.resolve(__dirname, "test"));
|
||||
|
||||
// this is a function to return directories, not files. So yeah.
|
||||
assert(!directories.includes("file.txt") && !directories.includes("another file.txt"));
|
||||
});
|
||||
it("throws when lacking first argument", function(){
|
||||
assert.throws(function(){
|
||||
let files = fileOps.getDirectoriesSync();
|
||||
}, Error);
|
||||
});
|
||||
});
|
||||
describe("fileOps.getFileNamesSync(path)", function(){
|
||||
it("gets names of files in directory", function(){
|
||||
let files = fileOps.getFileNamesSync(path.resolve(__dirname, "test"));
|
||||
assert(files.length > 0);
|
||||
assert(files.includes("file.txt") && files.includes("another file.txt"));
|
||||
});
|
||||
it("throws when lacking first argument", function(){
|
||||
assert.throws(function(){
|
||||
let files = fileOps.getFileNamesSync();
|
||||
}, Error);
|
||||
});
|
||||
});
|
||||
describe("fileOps.getNewestFile()", function(){
|
||||
it("gets the newest file in a directory async", function(done){
|
||||
fileOps.getNewestFile(path.resolve(__dirname, "test/"), fileOps.getFileNamesSync(path.resolve(__dirname, "test")), function(err, newest){
|
||||
assert(newest && typeof newest.file == "string");
|
||||
done();
|
||||
});
|
||||
});
|
||||
it("throws if callback is missing", function(){
|
||||
assert.throws(function(){
|
||||
fileOps.getNewestFile("test", fileOps.getFileNamesSync(path.resolve(__dirname, "test")));
|
||||
}, Error);
|
||||
});
|
||||
it("executes callback(err) if no files are provided", function(done){
|
||||
fileOps.getNewestFile(path.resolve(__dirname, "test"), [], function(err, newest){
|
||||
assert(err && typeof err == "object");
|
||||
done();
|
||||
});
|
||||
});
|
||||
it("executes callback(err) if launched without a directory", function(done){
|
||||
fileOps.getNewestFile("", fileOps.getFileNamesSync(path.resolve(__dirname, "test")), function(err, newest){
|
||||
assert(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it("executes callback(err) when you do stupid shit", function(done){
|
||||
fileOps.getNewestFile({thisWontWork: "but not crash either"}, {lolz:"yeah..."}, function(err, newest){
|
||||
assert(err);
|
||||
// ENOENT: no such file or directory, stat 'C:\**\factorioClusterio\lib\test\undefined'
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("fileOps.deleteFolderRecursiveSync(path)", function(){
|
||||
it("deletes a folder and all files and folders in it", function(){
|
||||
let x = fileOps.deleteFolderRecursiveSync(path.resolve(__dirname, "test"));
|
||||
assert.throws(function(){
|
||||
fs.statSync(path.resolve(__dirname, "test"));
|
||||
}, Error);
|
||||
assert(x);
|
||||
// give it some extra time because its sometimes very slow
|
||||
}).timeout(5000);
|
||||
it("returns false if you give it an invalid path", function(){
|
||||
let x = fileOps.deleteFolderRecursiveSync("This isn't a real path");
|
||||
assert(!x);
|
||||
});
|
||||
});
|
||||
describe("fileOps.deleteFolderRecursive(path)", function(){
|
||||
it("deletes a folder and all files and folders in it, just like sync version", async function(){
|
||||
setupTestingEnv();
|
||||
assert(fs.statSync(path.resolve(__dirname, "test")));
|
||||
let x = await fileOps.deleteFolderRecursive(path.resolve(__dirname, "test"));
|
||||
assert.throws(function(){
|
||||
fs.statSync(path.resolve(__dirname, "test"));
|
||||
}, Error);
|
||||
assert(x === undefined, "Promise version of this function does not have a return value upon success");
|
||||
return true;
|
||||
}).timeout(5000);
|
||||
it("throws like a promise does if you give it an invalid path", async function(){
|
||||
let x = await fileOps.deleteFolderRecursive("Look this path is fake!")
|
||||
.catch(e => {
|
||||
assert(e);
|
||||
assert.equal(e.code, "ENOENT");
|
||||
return true;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,2 +0,0 @@
|
||||
const fileOps = require("./fileOps");
|
||||
module.exports = fileOps;
|
||||
@@ -1,49 +0,0 @@
|
||||
var forge = require("node-forge");
|
||||
forge.options.usePureJavaScript = true;
|
||||
var fs = require("fs");
|
||||
var mkdirp = require("mkdirp");
|
||||
|
||||
var pki = forge.pki;
|
||||
var rsa = forge.pki.rsa;
|
||||
var startTime, endTime;
|
||||
|
||||
|
||||
|
||||
module.exports = function({
|
||||
bits = 2048,
|
||||
certificatePath = "database/certificate",
|
||||
doLogging = false,
|
||||
} = {}){
|
||||
function log(t){
|
||||
if(doLogging) console.log("lib/generateSSLcert.js | "+t);
|
||||
}
|
||||
log("Generating SSL certificate with "+bits+" bits at "+certificatePath);
|
||||
|
||||
startTime = Date.now();
|
||||
var keypair = rsa.generateKeyPair({bits: bits, e: 0x10001});
|
||||
endTime = Date.now();
|
||||
log("Generated 2048bit keypair in: "+(endTime-startTime)+" ms");
|
||||
|
||||
startTime = Date.now();
|
||||
var cert = pki.createCertificate();
|
||||
cert.publicKey = keypair.publicKey;
|
||||
cert.serialNumber = "01";
|
||||
|
||||
cert.validity.notBefore = new Date();
|
||||
cert.validity.notAfter = new Date(Date.now()+(1000*60*60*24*365)); // valid for one year
|
||||
|
||||
// self-sign certificate
|
||||
cert.sign(keypair.privateKey);
|
||||
|
||||
// Convert certificate to pem type and write it to a file
|
||||
if(certificatePath[certificatePath.length-1] != "/") certificatePath += "/";
|
||||
if(!fs.existsSync(certificatePath)) {
|
||||
mkdirp.sync(certificatePath);
|
||||
console.log("Created directory: "+certificatePath);
|
||||
}
|
||||
fs.writeFileSync(certificatePath+ "cert.key", pki.privateKeyToPem(keypair.privateKey));
|
||||
fs.writeFileSync(certificatePath+ "cert.crt", pki.certificateToPem(cert));
|
||||
|
||||
endTime = Date.now();
|
||||
log("Generated certificate in: "+(endTime-startTime)+" ms");
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
var assert = require("assert");
|
||||
var fs = require("fs");
|
||||
|
||||
var fileOps = require("lib/fileOps");
|
||||
var generateSSLcert = require("./generateSSLcert.js")
|
||||
|
||||
describe("generateSSLcert.js(options)", ()=>{
|
||||
it("Creates a folder with a .crt and .key file in it", ()=>{
|
||||
let certPath = "lib/certTestPlsDelete";
|
||||
assert(!fs.existsSync(certPath)); // if it already exsits, the test might pass because of leftover data (which is bad)
|
||||
|
||||
generateSSLcert({
|
||||
// bits:512, // supported, but I just use the default of 2048 for this test
|
||||
certificatePath:certPath,
|
||||
doLogging: false,
|
||||
});
|
||||
|
||||
assert(fs.existsSync(certPath));
|
||||
assert(fs.existsSync(certPath+"/cert.key")); // private key
|
||||
assert(fs.existsSync(certPath+"/cert.crt")); // certificate file
|
||||
|
||||
// remove certificates and data from the test
|
||||
fileOps.deleteFolderRecursiveSync(certPath);
|
||||
}).timeout(10000);
|
||||
});
|
||||
@@ -1,53 +0,0 @@
|
||||
/**
|
||||
Module for grabbing factorio locale from a local factorio install
|
||||
@module getFactorioLocale
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
const ini = require("ini");
|
||||
|
||||
const cache = {};
|
||||
|
||||
/**
|
||||
Gets raw factorio locale from the .ini file.
|
||||
Not available outside of module through exports.
|
||||
|
||||
@memberof getFactorioLocale
|
||||
@inner
|
||||
@param {string} factorioDirectory
|
||||
@param {string} languageCode
|
||||
@param {function} callback
|
||||
*/
|
||||
function getLocale(factorioDirectory, languageCode, callback){
|
||||
var localeFile = factorioDirectory + '/data/base/locale/' + languageCode + '/base.cfg';
|
||||
fs.readFile(localeFile, "utf8", (err, rawLocale) => {
|
||||
callback(err, rawLocale);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
Gets factorios locale as an object, does not respect mods.
|
||||
|
||||
@param {string} factorioDirectory
|
||||
@param {string} languageCode
|
||||
@param {function} callback
|
||||
*/
|
||||
module.exports.asObject = function getLocaleAsObject(factorioDirectory, languageCode, callback){
|
||||
if(typeof factorioDirectory != "string" || typeof languageCode != "string" || typeof callback != "function"){
|
||||
throw new Error("Error: wrong parameters provided");
|
||||
}
|
||||
|
||||
if(!cache[languageCode]){
|
||||
getLocale(factorioDirectory, languageCode, (err, rawLocale) => {
|
||||
if(err){
|
||||
callback(err);
|
||||
} else {
|
||||
let processedLocale = ini.parse(rawLocale);
|
||||
cache[languageCode] = processedLocale;
|
||||
callback(undefined, processedLocale);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
callback(undefined, cache[languageCode]);
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
const assert = require("assert");
|
||||
const getFactorioLocale = require("./getFactorioLocale");
|
||||
|
||||
describe("getFactorioLocale.js", () => {
|
||||
describe("asObject()", () => {
|
||||
it("requires 2 strings and a function as parameters", () => {
|
||||
let validate = (err) => {
|
||||
return err == "Error: wrong parameters provided"
|
||||
}
|
||||
let message = "getFactorioLocale is responding incorrectly to invalid arguments"
|
||||
let callback = (err,object) => true;
|
||||
|
||||
assert.throws(() => getFactorioLocale.asObject(), validate, message);
|
||||
assert.throws(() => getFactorioLocale.asObject("hello", "I forgot my callback"), validate, message);
|
||||
assert.throws(() => getFactorioLocale.asObject(undefined, undefined, callback), validate, message);
|
||||
|
||||
// the proper way to do it
|
||||
assert.doesNotThrow(() => getFactorioLocale.asObject("factorio_0.15.27", "en", callback), validate, message);
|
||||
});
|
||||
it("returns a nested object with base game locale information", (done) => {
|
||||
getFactorioLocale.asObject("factorio", "en", callback);
|
||||
|
||||
function callback(err, object){
|
||||
assert.equal(typeof object, "object");
|
||||
assert.equal(object["entity-name"]["fish"], "Fish");
|
||||
assert.equal(object["entity-name"]["small-lamp"], "Lamp");
|
||||
Object.keys(object).forEach(key => {
|
||||
// first level of the nested object is always an object
|
||||
assert.equal(typeof object[key], "object");
|
||||
|
||||
Object.keys(object[key]).forEach(key2 => {
|
||||
// second level of the nested object is always a string, nearly always truthy
|
||||
if(key2 != "so-long-and-thanks-for-all-the-fish"){
|
||||
assert.ok(object[key][key2]);
|
||||
assert.equal(typeof object[key][key2], "string");
|
||||
}
|
||||
});
|
||||
});
|
||||
done();
|
||||
}
|
||||
});
|
||||
it("has some duplicate keys", (done) => {
|
||||
getFactorioLocale.asObject("factorio", "en", callback);
|
||||
|
||||
function callback(err, object){
|
||||
let arrayOfKeys = [];
|
||||
// collect all keys in a single array
|
||||
Object.keys(object).forEach(key => {
|
||||
arrayOfKeys.push(key);
|
||||
Object.keys(object[key]).forEach(key2 => {
|
||||
arrayOfKeys.push(key2);
|
||||
});
|
||||
});
|
||||
|
||||
// check for duplicates
|
||||
let duplicateKeys = [];
|
||||
let i = 0;
|
||||
arrayOfKeys.forEach(key => {
|
||||
let o = 0;
|
||||
arrayOfKeys.forEach(key2 => {
|
||||
if(i != o && key == key2){
|
||||
duplicateKeys.push(key);
|
||||
}
|
||||
o++
|
||||
});
|
||||
i++;
|
||||
});
|
||||
// we got 704 but this is a very unexact number
|
||||
assert(duplicateKeys.length > 10)
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
const fileOps = require("lib/fileOps");
|
||||
// const config = require("./../config");
|
||||
|
||||
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
@@ -1,22 +0,0 @@
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves to the SHA1 hash of the file given by path
|
||||
*/
|
||||
exports.hashFile = function(path) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
let hasher = crypto.createHash('sha1')
|
||||
hasher.setEncoding('hex')
|
||||
hasher.on('finish', function() {
|
||||
resolve(hasher.read());
|
||||
});
|
||||
|
||||
let stream = fs.createReadStream(path);
|
||||
stream.on('error', function(error) {
|
||||
// the docs doesn't say anything about what's passed on error
|
||||
reject(error);
|
||||
});
|
||||
stream.pipe(hasher);
|
||||
});
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
module.exports = require("./isFactorioCommand.js");
|
||||
@@ -1,23 +0,0 @@
|
||||
const luamin = require("luamin");
|
||||
function isFactorioCommand(command){
|
||||
if(typeof command != "string"){
|
||||
return false;
|
||||
} else if(!command.includes("/silent-command ") && !command.includes("/c ")){
|
||||
return false;
|
||||
} else if(command[0] != "/"){
|
||||
return false;
|
||||
} else {
|
||||
// check that what follows /c is valid LUA
|
||||
let x = command.replace("/silent-command ", "").replace("/c ","");
|
||||
let y
|
||||
try{
|
||||
y = luamin.minify(x);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// by this point it is surely a LUA command
|
||||
return true;
|
||||
}
|
||||
}
|
||||
module.exports = isFactorioCommand;
|
||||
@@ -1,18 +0,0 @@
|
||||
const isFactorioCommand = require("./isFactorioCommand");
|
||||
const assert = require("assert");
|
||||
|
||||
describe("inventoryImports/isFactorioCommand.js()", function(){
|
||||
it("Returns false if input is not a factorio command", function(){
|
||||
assert(isFactorioCommand({}) == false, "This is not even a string so should return false");
|
||||
assert(isFactorioCommand("Howdy boys!") == false, "This is not a command so should return false");
|
||||
assert(isFactorioCommand("/c this ain't a command because its invalid LUA") == false, "This is not valid LUA so it should return false");
|
||||
assert(isFactorioCommand("Trying to trick the system /c game.print('Hello world!')") == false, "this is still not a command");
|
||||
|
||||
});
|
||||
it("Returns true if it is a valid command", function(){
|
||||
assert(isFactorioCommand("/c game.print('hello world')"), "This is a valid command, should output true not false")
|
||||
});
|
||||
it("Fails to understand that leading spaces are valid", function(){
|
||||
assert(isFactorioCommand(" /c game.print('Hello world!')") == false, "This should be true but often returns false unintentionally. If you see this you probably fixed the bug so change this test, OK?")
|
||||
});
|
||||
});
|
||||
@@ -1,69 +0,0 @@
|
||||
/**
|
||||
Provides tools for managing both masters and slaves config files.
|
||||
@module configManager
|
||||
*/
|
||||
// require modules
|
||||
const asTable = require("as-table").configure({delimiter: ' | '});
|
||||
const fs = require("fs");
|
||||
|
||||
// internal requires
|
||||
var config = require("./../../config");
|
||||
const objectOps = require("./../objectOps.js");
|
||||
/**
|
||||
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=>{
|
||||
if(key != "__comment"){
|
||||
if(typeof config[key] == "object"){
|
||||
configArray.push({entry: key, value: JSON.stringify(config[key])});
|
||||
} else configArray.push({entry: key, value: config[key]});
|
||||
}
|
||||
});
|
||||
return configArray;
|
||||
}
|
||||
}
|
||||
module.exports.editConfig = function(instance, entry, newValue){
|
||||
if(!instance || instance == "shared"){
|
||||
if(config[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, " "))
|
||||
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, " "))
|
||||
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`,
|
||||
}
|
||||
}
|
||||
};
|
||||
Whitespace-only changes.
@@ -1,2 +0,0 @@
|
||||
const fs = require("fs-extra");
|
||||
const needle = require("needle");
|
||||
@@ -1,277 +0,0 @@
|
||||
/**
|
||||
Provides tools to manage mods in a clusterio install
|
||||
@module modManager
|
||||
*/
|
||||
|
||||
const asTable = require("as-table").configure({delimiter: ' | '});
|
||||
const moment = require("moment");
|
||||
const syncRequest = require("sync-request");
|
||||
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("lib/fileOps");
|
||||
// const config = require("./../../config");
|
||||
|
||||
|
||||
function ellipse(str, max){
|
||||
return str.length > (max - 3) ? str.substring(0,max-3) + '...' : str;
|
||||
}
|
||||
// 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]};
|
||||
|
||||
|
||||
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
|
||||
*/
|
||||
listMods: function(instance){
|
||||
return new Promise((resolve, reject) => {
|
||||
if(instance && instances.includes(instance)){
|
||||
factorioAPI.init(false, config.instanceDirectory+'/'+instance+'/instanceMods', config.instanceDirectory+'/'+instance+'/saves', config.factorio_version);
|
||||
factorioAPI.authenticate({
|
||||
username: config.username,
|
||||
token: config.token,
|
||||
require_ownership: false
|
||||
}).then(token => {
|
||||
console.log("* Currently installed:");
|
||||
factorioAPI.readModZips().then(list => {
|
||||
console.log(asTable(list.map(mod => {
|
||||
return {
|
||||
name:(mod.title || mod.name),
|
||||
version:mod.version,
|
||||
date:mod.date,
|
||||
factorio_ver:mod.factorio_version,
|
||||
author:mod.author,
|
||||
}
|
||||
})));
|
||||
});
|
||||
// Had issues earlier due to bug in node-factorio-api, see https://github.com/Danacus/node-factorio-api/issues/2
|
||||
// bug is resolved, thanks to @Danacus and @brotherpot
|
||||
factorioAPI.getModsFromSaves().then(list => {
|
||||
console.log("* Mods used in save:");
|
||||
console.log(asTable(list[0].mods))
|
||||
});
|
||||
}).catch(reason => {
|
||||
console.log("Failed to authenticate to factorio.com! Please check your connection and the username+token used for authentication in config.json");
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
// list shared mods in /sharedMods/
|
||||
factorioAPI.init(false, 'sharedMods/', '', config.factorio_version);
|
||||
factorioAPI.authenticate({
|
||||
username: config.username,
|
||||
token: config.token,
|
||||
require_ownership: false
|
||||
}).then(() => {
|
||||
console.log("\r\n")
|
||||
console.log("* Mods shared by all instances:")
|
||||
factorioAPI.getInstalledMods().then(mods => {
|
||||
console.log(asTable(mods));
|
||||
resolve();
|
||||
});
|
||||
}).catch(e => {
|
||||
reject(new Error(e));
|
||||
});
|
||||
}
|
||||
|
||||
// get JSON data about GH releases
|
||||
/*let res = syncRequest('GET', 'https://api.github.com/repos/Danielv123/factorioClusterioMod/releases', {"headers":{"User-Agent":"Fuck you for requiring user agents!"}});
|
||||
let url = JSON.parse(res.getBody())[0].assets[0].browser_download_url;
|
||||
let name = JSON.parse(res.getBody())[0].assets[0].name;
|
||||
console.log(JSON.parse(res.getBody()))
|
||||
*/
|
||||
});
|
||||
},
|
||||
/**
|
||||
Searches mods.factorio.com for mods matching a search term and prints
|
||||
the results to console as a neat looking table
|
||||
|
||||
@param {string} searchTerm What to search for
|
||||
*/
|
||||
findMods: function(searchTerm){
|
||||
return new Promise((resolve, reject) => {
|
||||
factorioAPI.init(false);
|
||||
factorioAPI.authenticate({
|
||||
username: config.username,
|
||||
token: config.token,
|
||||
require_ownership: false
|
||||
}).then(token => {
|
||||
factorioAPI.searchMods(searchTerm).then(mods => {
|
||||
let result = asTable(mods.reverse().map(mod => {
|
||||
return {
|
||||
title:ellipse(mod.title, 50),
|
||||
name:ellipse(mod.name, 30),
|
||||
downloads:mod.downloads_count,
|
||||
updated: moment(mod.updated_at).format("DD.MM.YY"),
|
||||
latest_version: mod.latest_release.version,
|
||||
owner:mod.owner,
|
||||
//license: mod.license_name,
|
||||
}
|
||||
}));
|
||||
resolve(result);
|
||||
// console.log(result);
|
||||
});
|
||||
}).catch(e => {
|
||||
if(e.contains("Insufficient information")) console.log("We are missing some parameters required for logging in! Make sure your factorio username and token are specified in config.json.");
|
||||
reject(new Error(e));
|
||||
});
|
||||
});
|
||||
},
|
||||
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);
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
/*
|
||||
module.exports.findUpdates = function(){
|
||||
console.log("Downloading mods...");
|
||||
// get JSON data about 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()))
|
||||
}*/
|
||||
Whitespace-only changes.
@@ -1,116 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
var mkdirp = require("mkdirp");
|
||||
var http = require('http');
|
||||
var https = require('https');
|
||||
var fs = require('fs');
|
||||
|
||||
var modManager = require("./manager/modManager.js");
|
||||
|
||||
// modManager.addMod("clusterio", "shared");
|
||||
|
||||
function makeDirectory(path){
|
||||
return new Promise((resolve, reject) => {
|
||||
mkdirp(path, function(err){
|
||||
if(err){
|
||||
console.error("Unable to mkdirp "+path);
|
||||
console.error(err);
|
||||
resolve(false);
|
||||
} else {
|
||||
console.log("mkdirp "+path);
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function download(url, file, callback){
|
||||
return new Promise((resolve, reject) => {
|
||||
let pathArray = file.split("/");
|
||||
pathArray.pop(); // remove filename from the end
|
||||
if(pathArray.length >= 1){
|
||||
// this might be an invalid path!
|
||||
fs.stat(pathArray.join("/"), (err, stat) => {
|
||||
if(err){
|
||||
makeDirectory(pathArray.join("/")).then(()=>{
|
||||
doDownload(url, file, callback);
|
||||
});
|
||||
} else {
|
||||
doDownload(url, file, callback);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
doDownload(url, file, callback);
|
||||
}
|
||||
function doDownload(url, file, callback){
|
||||
var writeStream = fs.createWriteStream(file);
|
||||
let protocol = http;
|
||||
if(url.split(":")[0] == "https") protocol = https;
|
||||
var request = protocol.get(url, function(response) {
|
||||
response.pipe(writeStream);
|
||||
response.on("end", () => {
|
||||
if(callback && typeof callback == "function") callback(file);
|
||||
console.log("Downloaded "+file+" from "+url);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var directories = [
|
||||
"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: "https://code.jquery.com/jquery.js",
|
||||
file: "static/external/jquery.js",
|
||||
},
|
||||
{
|
||||
url: "https://code.jquery.com/ui/1.12.1/jquery-ui.js",
|
||||
file: "static/external/jquery-ui.js",
|
||||
},
|
||||
{
|
||||
url: "https://cdnjs.cloudflare.com/ajax/libs/canvasjs/1.7.0/canvasjs.js",
|
||||
file: "static/external/canvasjs.js",
|
||||
},
|
||||
{
|
||||
url: "https://momentjs.com/downloads/moment.min.js",
|
||||
file: "static/external/moment.min.js",
|
||||
},
|
||||
{
|
||||
url: "https://craig.global.ssl.fastly.net/js/mousetrap/mousetrap.min.js",
|
||||
file: "static/external/mousetrap.min.js",
|
||||
},
|
||||
{
|
||||
url: "https://fonts.googleapis.com/css?family=Roboto",
|
||||
file: "static/external/css/Roboto.css",
|
||||
},
|
||||
{
|
||||
url: "https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.4/socket.io.js",
|
||||
file: "static/external/socket.io.js",
|
||||
},
|
||||
]
|
||||
|
||||
// initiate folder creations and downloads
|
||||
processMkdirRecursive(processDownloadRecursive);
|
||||
function processMkdirRecursive(callback){
|
||||
let item = directories.shift();
|
||||
makeDirectory(item).then(()=>{
|
||||
if(directories.length == 0){
|
||||
if(callback) callback();
|
||||
} else {
|
||||
processMkdirRecursive(callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
function processDownloadRecursive(callback){
|
||||
let item = downloadList.shift();
|
||||
download(item.url, item.file, item.callback).then(()=>{
|
||||
if(downloadList.length == 0){
|
||||
if(callback) callback();
|
||||
} else {
|
||||
processDownloadRecursive(callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// generate SSL certificates if they are missing
|
||||
if(!fs.existsSync("database/certificates") || !fs.existsSync("database/certificates/cert.crt") || !fs.existsSync("database/certificates/cert.key")){
|
||||
var generateSSLcert = require("./generateSSLcert.js");
|
||||
|
||||
generateSSLcert({
|
||||
certificatePath: "database/certificates",
|
||||
doLogging: true,
|
||||
});
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
/**
|
||||
Helper functions related to objects
|
||||
@module objectOps
|
||||
*/
|
||||
|
||||
/**
|
||||
Deep clones an object, is actually just JSON.parse(JSON.stringify()) but looks cleaner.
|
||||
@param {object} obj to clone
|
||||
@returns {object}
|
||||
|
||||
@example
|
||||
a = {"thingy":"with a value"};
|
||||
b = a;
|
||||
a == b; // true
|
||||
c = objectOps.deepclone(a);
|
||||
a == c; // false
|
||||
*/
|
||||
module.exports.deepclone = function(obj){
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
/**
|
||||
Checks if a string is valid JSON using try/catch. If passed an object it will always return true.
|
||||
@param {string} string to verify
|
||||
@returns {boolean}
|
||||
|
||||
@example
|
||||
objectOps.isJSON("99 bottles of beer"); // false
|
||||
objectOps.isJSON("99"); // true
|
||||
objectOps.isJSON({"thingy":"that is an object"}); // true
|
||||
objectOps.isJSON(JSON.stringify({"thingy":"that is an object"})); // true
|
||||
*/
|
||||
module.exports.isJSON = function(string){
|
||||
let stringIsJson = false;
|
||||
let x
|
||||
try {
|
||||
x = JSON.parse(string);
|
||||
} catch (e){
|
||||
return false;
|
||||
}
|
||||
if(typeof x == "object" || typeof string == "object"){
|
||||
stringIsJson = true;
|
||||
}
|
||||
return stringIsJson;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
var assert = require('assert');
|
||||
var objectOps = require('./objectOps');
|
||||
|
||||
describe("objectOps.js", function(){
|
||||
/*describe("objectOps.clone()", function(){
|
||||
it("clones javascript objects", function(){
|
||||
var object1 = {hello:"world", cat:{legs:4, name:"Kitty", colors:["brown", "yellow", "purple"]}};
|
||||
var object2 = {};
|
||||
object2 = objectOps.clone(object1);
|
||||
|
||||
assert.equal(object2.cat.name, object1.cat.name)
|
||||
assert.equal(object1.hello, object2.hello)
|
||||
});
|
||||
it("does not deep clone objects", function(){
|
||||
var obj1 = {hello:"world", cat:{legs:4, name:"Kitty", colors:["brown", "yellow", "purple"]}};
|
||||
var obj2 = {};
|
||||
obj2 = objectOps.clone(obj1);
|
||||
|
||||
obj1.cat.colors = "black";
|
||||
assert.equal(obj1.cat.colors, "black");
|
||||
assert.notEqual(obj2.cat.colors[1], "yellow");
|
||||
});
|
||||
});*/
|
||||
describe("objectOps.deepclone()", function(){
|
||||
it("deep clones javascript objects", function(){
|
||||
var obj1 = {hello:"world", cat:{legs:4, name:"Kitty", colors:["brown", "yellow", "purple"]}};
|
||||
var obj2 = {};
|
||||
obj2 = objectOps.deepclone(obj1);
|
||||
|
||||
obj1.cat.colors = "black";
|
||||
assert.equal(obj1.cat.colors, "black");
|
||||
assert.equal(obj2.cat.colors[1], "yellow");
|
||||
});
|
||||
it("throws on non JSON parameters", function(){
|
||||
assert.throws(function(){
|
||||
let y = objectOps.deepclone(objectOps.deepclone);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "lib",
|
||||
"description": "Internal library for factorioClusterio"
|
||||
}
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
--[[
|
||||
|
||||
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
|
||||
@@ -1,135 +0,0 @@
|
||||
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
|
||||
@@ -1,377 +0,0 @@
|
||||
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
@@ -1,322 +0,0 @@
|
||||
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
|
||||
@@ -1,3 +0,0 @@
|
||||
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
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
msg-intro=.بكوكلا اذه ناكس م كسفن يمحت نا ىسنت الو ,ةيكيتاموتوألا ربع كقيرط ينبتسو ,رفصلا نم أدبتس .كلت خيراوصلا قالطإ ةطحم حتف عيطتست فيكل ةروطتملا ايجولونكتلل ثاحبأب مايقلا ىلإ جاتحتس .اهنم خوراص قالطإ مث خيراوص قالطإ ةطحم ءانب قيرط نع كلذب مايقلا كنكمي ,ءاضفلا ىلإ اخوراص قلطت نا يه كتمهم ,factorioـل رحلا بعللا روط وه اذه
|
||||
scenario-name=رحلا بعللا
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
msg-intro=Гэта рэжым свабоднай гульні Factorio. Вашая мэта - запусціць ракету ў космас. Дзеля гэтага вам трэба пабудаваць ракетную шахту і саму ракету са спадарожнікам. Вы мусіце даследаваць перадавыя тэхналогіі, каб навучыцца будаваць ракетную шахту. Пачынайце з малога, працуйце над аўтаматызацыяй вытворчасці, даследуйце новыя тэхналогіі і не забывайцеся на абарону ад «тутэйшых».
|
||||
scenario-name=Свабодная гульня
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
msg-intro=Това е режимът на свободна игра. Задачата ви е да изстреляте ракета в космоса. За това ще е нужно да построите ракетна шахта и изстреляте ракетата със спътник. Ще трябва да проучите развити технологии, които ще отключат ракетната шахта. Започнете с малко и постепенно автоматизирайте процеса, като не забравяте да се защитавате от местните форми на живот.
|
||||
scenario-name=Свободна игра
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
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
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
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
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
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
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
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
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
msg-intro=Αυτό είναι το ελεύθερο παιχνίδι του Factorio. Ο στόχος σας είναι να εκτοξεύσετε έναν πύραυλο στο διάστημα. Αυτό το κάνετε κατασκευάζοντας ένα σιλό πυραύλου και εκτοξεύοντας έναν πύραυλο. Θα πρέπει να ερευνήσετε προηγμένες τεχνολογίες για να ξεκλειδώσετε το σιλό πυραύλου. Ξεκινήστε χαμηλά, φτάστε ψηλά χρησιμοποιώντας αυτοματισμούς και μην ξεχάσετε να προστατέψετε τον εαυτό σας από τους ντόπιους.
|
||||
scenario-name=Ελεύθερο παιχνίδι
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
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
|
||||
@@ -1,81 +0,0 @@
|
||||
[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
|
||||
@@ -1,2 +0,0 @@
|
||||
scenario-name=Libera Ludado
|
||||
|
||||
Loaded 100 of 4012 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user