Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc2625a7db | ||
|
|
2e756b1649 | ||
|
|
55df1bae8a | ||
|
|
a4ee6f82cb | ||
|
|
9f3978e0b5 | ||
|
|
e731fcb4a9 | ||
|
|
9377eb9544 | ||
|
|
f6afa488d1 | ||
|
|
936600c98b | ||
|
|
7352ee3751 | ||
|
|
3a87aa2959 |
No files matched your search
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"image":"mcr.microsoft.com/devcontainers/universal:2",
|
||||
"postCreateCommand": "mkdir factorio && cd factorio && wget https://factorio.com/get-download/1.1.107/headless/linux64 && tar -xf linux64 && mv factorio factorio_1.1.107 && rm linux64 && wget https://factorio.com/get-download/2.0.47/headless/linux64 && tar -xf linux64 && mv factorio factorio_2.0.47 && rm linux64 && pnpm install",
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
# No need to invalidate cache just because we try speeding up the build
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
docker-compose*
|
||||
|
||||
# Install dependencies during the build
|
||||
node_modules
|
||||
packages/**/node_modules
|
||||
plugins/**/node_modules
|
||||
|
||||
# Download factorio during the build
|
||||
factorio
|
||||
factorio.tar.gz
|
||||
|
||||
# We build internal mods during build, mods from local test env should not be applied
|
||||
mods
|
||||
|
||||
# These files should never be required during the build
|
||||
.git
|
||||
.github
|
||||
dist
|
||||
temp
|
||||
@@ -1 +0,0 @@
|
||||
package-lock.json -diff
|
||||
@@ -1,17 +0,0 @@
|
||||
Describe why and what has been changed by this pullrequest, feel free to also ask for input on decisions you are unsure about etc.
|
||||
The changelog lines below will be copied directly from merged pull requests to the changelog on new releases. Changes are expected to preferrentially link to the associated issues. If there are no relevant issues, link to the pull request itself.
|
||||
|
||||
## Changelog
|
||||
```
|
||||
### Features
|
||||
- Added doodad to web interface. [#999](https://github.com/clusterio/clusterio/issues/999)
|
||||
|
||||
### Changes
|
||||
- ...
|
||||
|
||||
### Fixes
|
||||
- ...
|
||||
|
||||
### Breaking changes
|
||||
- ...
|
||||
```
|
||||
@@ -1,77 +0,0 @@
|
||||
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: Node.js CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
# Supported versions are active lts and maintenance see https://nodejs.org/en/about/releases/
|
||||
node-version: [20.x, 22.x]
|
||||
# Supported versions are latest stable and one minor version before stable (even if from a previous major)
|
||||
factorio-version: [1.1.110, 2.0.47]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Prepare and install
|
||||
run: pnpm i --no-frozen-lockfile
|
||||
- name: Use Factorio ${{ matrix.factorio-version }}
|
||||
run: wget -q -O factorio.tar.gz https://www.factorio.com/get-download/${{ matrix.factorio-version }}/headless/linux64 && tar -xf factorio.tar.gz && rm factorio.tar.gz
|
||||
- name: Run tests
|
||||
if: ${{ matrix.node-version != '20.x' || matrix.factorio-version != '1.1.110' }}
|
||||
run: pnpm test
|
||||
- name: Run coverage
|
||||
if: ${{ matrix.node-version == '20.x' && matrix.factorio-version == '1.1.110' }}
|
||||
run: pnpm run ci-cover
|
||||
- name : Upload coverage to Codecov
|
||||
if: ${{ matrix.node-version == '20.x' && matrix.factorio-version == '1.1.110' }}
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: ./coverage/lcov.info
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
- run: pnpm i --no-frozen-lockfile
|
||||
- run: pnpm run lint
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
- run: pnpm i --no-frozen-lockfile
|
||||
- run: pnpm run build-mod
|
||||
# Upload build to artifacts
|
||||
- name: Upload build to artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: clusterio_lib
|
||||
path: ./dist
|
||||
if-no-files-found: error
|
||||
@@ -1,48 +0,0 @@
|
||||
|
||||
name: Publish
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- released
|
||||
|
||||
jobs:
|
||||
npm:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.x
|
||||
registry-url: https://registry.npmjs.org
|
||||
- run: pnpm install --no-frozen-lockfile
|
||||
- run: pnpm -r --filter='./{plugins,packages}/*' publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: $${{ secrets.NPM_TOKEN }}
|
||||
- uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: pnpm-lock.yaml
|
||||
|
||||
factorio:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.x
|
||||
- run: pnpm install --no-frozen-lockfile
|
||||
- run: pnpm -r --filter='./{plugins,packages}/*' run build-mod --output-dir ../../dist
|
||||
- run: node ./publish.js factorio ./dist
|
||||
env:
|
||||
FACTORIO_TOKEN: ${{ secrets.FACTORIO_TOKEN }}
|
||||
- uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: ./dist/*
|
||||
@@ -1,6 +1,7 @@
|
||||
# Logs
|
||||
logs
|
||||
*debug.log*
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
@@ -10,9 +11,8 @@ pids
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directories used by tools like nyc and codecov
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
@@ -22,31 +22,48 @@ coverage
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
dist/
|
||||
|
||||
# Autogenerated docs output
|
||||
/docs/api
|
||||
|
||||
node_modules
|
||||
node_modules/*
|
||||
!node_modules/_app
|
||||
|
||||
Factorio*
|
||||
/factorio
|
||||
!/test/file/factorio
|
||||
!/test/lib/factorio
|
||||
!/packages/lib/src/factorio
|
||||
/instances
|
||||
/mods
|
||||
/sharedMods
|
||||
/database
|
||||
/plugin-list.json
|
||||
/config-controller.json
|
||||
/config-control.json
|
||||
/config-host.json
|
||||
pnpm-lock.yaml
|
||||
database/items.db
|
||||
config.json
|
||||
package-lock.json
|
||||
secret-api-token.txt
|
||||
mod_cache.json
|
||||
/static/
|
||||
static/pictures/spritesheet.js
|
||||
static/pictures/spritesheet.png
|
||||
static/external
|
||||
saves
|
||||
/temp/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vs/
|
||||
# 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
|
||||
|
||||
\.cache-require-paths\.json
|
||||
|
||||
\.vs/
|
||||
|
||||
bin/
|
||||
@@ -0,0 +1,8 @@
|
||||
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
|
||||
version: v1.12.0
|
||||
ignore: {}
|
||||
# patches apply the minimum changes required to fix a vulnerability
|
||||
patch:
|
||||
'npm:lodash:20180130':
|
||||
- html5-validator > caporal > cli-table2 > lodash:
|
||||
patched: '2018-07-03T06:07:55.532Z'
|
||||
@@ -0,0 +1,10 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "9"
|
||||
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
|
||||
script:
|
||||
- npm run cover
|
||||
@@ -1,3 +0,0 @@
|
||||
*
|
||||
!.gitignore
|
||||
!settings.json
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"editor.detectIndentation": false,
|
||||
"editor.insertSpaces": false,
|
||||
"files.eol": "\n",
|
||||
"files.insertFinalNewline": true,
|
||||
"files.trimFinalNewlines": true,
|
||||
"[markdown]": {
|
||||
"editor.indentSize": 4,
|
||||
"editor.insertSpaces": true,
|
||||
},
|
||||
}
|
||||
@@ -1,598 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
<!-- Template for entries. Add sections that apply as changes are made.
|
||||
## Version 2.0.0-alpha.xx
|
||||
|
||||
### Major Features
|
||||
|
||||
- Major additions and/or major improvements to managing clusters.
|
||||
|
||||
### Features
|
||||
|
||||
- New features added.
|
||||
|
||||
### Changes
|
||||
|
||||
- Small changes to existing behaviour.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Bugfixes
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Changes which break backwards compatibility with older hosts or plugins code.
|
||||
|
||||
### Meta
|
||||
|
||||
- Changes to the project activities / structure worth highlighting but have no impact on the published package.
|
||||
|
||||
Many thanks to the following for contributing to this release:
|
||||
[@username](https://github.com/username)
|
||||
-->
|
||||
|
||||
## Version 2.0.0-alpha.22
|
||||
|
||||
### Major Features
|
||||
|
||||
- Added a download dependencies button which downloads missing dependencies. [#726](https://github.com/clusterio/clusterio/issues/726)
|
||||
- Added an update mod button which downloads the latest compatible version of a mod. [#725](https://github.com/clusterio/clusterio/issues/725)
|
||||
|
||||
### Features
|
||||
|
||||
- Added online player list to instance details page. [#666](https://github.com/clusterio/clusterio/issues/666)
|
||||
- Added ability to restart instances as a single action. [#779](https://github.com/clusterio/clusterio/issues/779)
|
||||
- Added ability to exclude instances from the 'Start all' web UI button. [#451](https://github.com/clusterio/clusterio/issues/451)
|
||||
- Added plugin update detection for restart required [#759](https://github.com/clusterio/clusterio/issues/759)
|
||||
- Added detection for mismatched mod and mod pack factorio version. [#727](https://github.com/clusterio/clusterio/issues/727)
|
||||
- Added detection for dependency issues such as incompatible or wrong version. [#727](https://github.com/clusterio/clusterio/issues/727)
|
||||
- Added a quick fix button for resolving detected dependency issues. [#801](https://github.com/clusterio/clusterio/issues/801)
|
||||
|
||||
### Changes
|
||||
|
||||
- Restart host / controller is now the default action with stop moved to a dropdown. [#780](https://github.com/clusterio/clusterio/issues/780)
|
||||
- Restart host / controller no longer shows unless it is enabled for that machine. [#780](https://github.com/clusterio/clusterio/issues/780)
|
||||
- Plugin templates no longer depend on a dev install of Clusterio. [#786](https://github.com/clusterio/clusterio/issues/786)
|
||||
- Better error message for when a plugin is missing an index file. [#799](https://github.com/clusterio/clusterio/pull/799)
|
||||
- `ModStore.downloadMods` no longer checks if the mod is already downloaded. [#801](https://github.com/clusterio/clusterio/issues/801
|
||||
- Mod version for buildins will always be fully qualified, matching the requirement enforced on other mods. [#801](https://github.com/clusterio/clusterio/issues/801))
|
||||
- Subscriptions can now be filtered. [#788](https://github.com/clusterio/clusterio/pull/788)
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fixed the missing space between action buttons on the users and hosts list. [#780](https://github.com/clusterio/clusterio/issues/780)
|
||||
- Fixed "rendered more hooks" error when refreshing hosts page. [#769](https://github.com/clusterio/clusterio/issues/769)
|
||||
- Fixed inventory combinator time signal not always updating [#610](https://github.com/clusterio/clusterio/issues/610)
|
||||
- Renamed templates for `.gitignore` and `.npmignore` that caused `create --plugin-template` to fail. [#800](https://github.com/clusterio/clusterio/pull/800)
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- `ModPortalDownloadRequest` now requires an array of mod version pairs rather than a single mod name and version. [#801](https://github.com/clusterio/clusterio/issues/801)
|
||||
- Version strings (e.g. `1.0`, `1.0.0`) now have stricter typing and validation, passing unchecked strings to functions will error. [#801](https://github.com/clusterio/clusterio/issues/801)
|
||||
|
||||
Many thanks to the following for contributing to this release:
|
||||
[@Cooldude2606](https://github.com/Cooldude2606)
|
||||
[@Danielv123](https://github.com/Danielv123)
|
||||
[@Hornwitser](https://github.com/Hornwitser)
|
||||
|
||||
## Version 2.0.0-alpha.21
|
||||
|
||||
### Major Features
|
||||
|
||||
- Added import, export, and restore of user lists (bans / admins / whitelist / combined). [#717](https://github.com/clusterio/clusterio/issues/717)
|
||||
- Added remote update for core packages and third party plugins. [#761](https://github.com/clusterio/clusterio/issues/761)
|
||||
- Added config cloning on instance creation. [#720](https://github.com/clusterio/clusterio/issues/720)
|
||||
|
||||
### Features
|
||||
|
||||
- Added social links and instructions to error page. [#742](https://github.com/clusterio/clusterio/issues/742)
|
||||
- Added button to download control config. [#740](https://github.com/clusterio/clusterio/issues/740)
|
||||
- Added server action filter (aka "chat filter") to instance log in web ui. [#744](https://github.com/clusterio/clusterio/issues/744)
|
||||
- Added `factorio.console_logging` to enable dedicated console log file for servers. [#748](https://github.com/clusterio/clusterio/issues/748)
|
||||
- Added a recovery mode to host and controller accessible as an option on the run command. [#752](https://github.com/clusterio/clusterio/issues/752)
|
||||
- Added username search to the users list. [#751](https://github.com/clusterio/clusterio/issues/751)
|
||||
- Added log level filtering to the web ui. [#745](https://github.com/clusterio/clusterio/issues/745)
|
||||
- Added pending update detection for host and controller. [#758](https://github.com/clusterio/clusterio/issues/758)
|
||||
- Added restart required indicator for host and controller. [#758](https://github.com/clusterio/clusterio/issues/758)
|
||||
- Added version mismatch indicator in host list. [#758](https://github.com/clusterio/clusterio/issues/758)
|
||||
- Added host plugin list command to crl where previously only controller plugin list existed. [#760](https://github.com/clusterio/clusterio/issues/760)
|
||||
- Added optional feature which allows new plugins to be installed remotely.
|
||||
- Added bidirectional whitelist syncing. [#716](https://github.com/clusterio/clusterio/issues/716)
|
||||
- Added support for v2 map exchange strings. [#700](https://github.com/clusterio/clusterio/issues/700)
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fixed handling of zip files with multiple top-level folders. [#739](https://github.com/clusterio/clusterio/issues/739)
|
||||
- Fixed looping between instances of ban and admin sync events. [#736](https://github.com/clusterio/clusterio/issues/736)
|
||||
- Fixed quality being stripped from power armour equipment when moving between servers [#753](https://github.com/clusterio/clusterio/pull/753)
|
||||
- Resolved webpack type export warnings [#750](https://github.com/clusterio/clusterio/pull/750)
|
||||
- Reduce page load time on users list by enabling pagination. [#718](https://github.com/clusterio/clusterio/issues/718)
|
||||
- Corrected stack trace and enabled source mapping during bootstrap. [#764](https://github.com/clusterio/clusterio/pull/764)
|
||||
- Fixed map gen settings using the incorrect name for cliff settings following exchange string decode.
|
||||
|
||||
### Meta
|
||||
|
||||
- Silent testing is now truly silent. [#764](https://github.com/clusterio/clusterio/pull/764)
|
||||
- Node 18 has reached end of life and is no longer maintained, we no longer guarantee support for it.
|
||||
- CI testing now runs on node 20 & 22 as well as factorio 1.1.110 & 2.0.47.
|
||||
|
||||
Many thanks to the following for contributing to this release:
|
||||
[@Cooldude2606](https://github.com/Cooldude2606)
|
||||
[@Danielv123](https://github.com/Danielv123)
|
||||
[@Hornwitser](https://github.com/Hornwitser)
|
||||
|
||||
## Version 2.0.0-alpha.20
|
||||
|
||||
### Features
|
||||
|
||||
- Update subspace storage to 2.0 and space age. [#733](https://github.com/clusterio/clusterio/pull/733)
|
||||
- A fresh install of Clusterio will include default mod packs. [#719](https://github.com/clusterio/clusterio/issues/719)
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix crash when leaving a server without a logistic requester point [#731](https://github.com/clusterio/clusterio/pull/731)
|
||||
- Use floor instead of round to prevent "1 minute and 60 seconds" being shown [#723](https://github.com/clusterio/clusterio/issues/723)
|
||||
- Fixed production statistics overwriting consumption statistics and vise versa [#734](https://github.com/clusterio/clusterio/pulls/734)
|
||||
- Hot patch to add the updated 2.0.29 control.lua freeplay hash to know scenarios. [commit](https://github.com/clusterio/clusterio/commit/26269df7d4d8f39042c59653008bc8f034a4f3b4)
|
||||
|
||||
### Meta
|
||||
|
||||
- We have now introduced a new [decisions log](https://github.com/clusterio/clusterio/blob/master/docs/decisions.md) to aid tracking all the technical and design decisions we make and their reasons. [#714](https://github.com/clusterio/clusterio/pull/714)
|
||||
|
||||
Many thanks to the following for contributing to this release:
|
||||
[@Cooldude2606](https://github.com/Cooldude2606)
|
||||
[@Danielv123](https://github.com/Danielv123)
|
||||
[@psihius](https://github.com/psihius)
|
||||
|
||||
## Version 2.0.0-alpha.19
|
||||
|
||||
### Major Features
|
||||
|
||||
- Added support for 2.0 builtin mods during mod pack creation. [#684](https://github.com/clusterio/clusterio/pull/684)
|
||||
- Added basic 2.0 support to clusterio lua api and 1st party plugins. [#685](https://github.com/clusterio/clusterio/pull/685)
|
||||
|
||||
### Features
|
||||
|
||||
- Added Datastore and Datastore Provider classes to the library to support savable Maps. [#629](https://github.com/clusterio/clusterio/pull/629)
|
||||
- Added clean script to speed up publishing and debugging of build steps. [#640](https://github.com/clusterio/clusterio/pull/640)
|
||||
- Added sub-tile option on plugin page headers. [#642](https://github.com/clusterio/clusterio/pull/642)
|
||||
- Greatly improved plugin and module template generation. [#641](https://github.com/clusterio/clusterio/pull/641)
|
||||
- Duplicate host connections from the same address will kill the older host process. [#647](https://github.com/clusterio/clusterio/pull/647)
|
||||
- Controller will refuse duplicate host connections from different addresses. [#647](https://github.com/clusterio/clusterio/pull/647)
|
||||
- Monorepos are now supported for external plugins. [#654](https://github.com/clusterio/clusterio/pull/654)
|
||||
- Changed default role selection to be a dropdown menu rather than id. [#664](https://github.com/clusterio/clusterio/pull/664)
|
||||
- Added credential option to config entries to allow writing sensitive data that can't be read back remotely.
|
||||
- Allow hidden and readonly graphical representations of config values. [#665](https://github.com/clusterio/clusterio/pull/665)
|
||||
- Added locale exporting for all builtin mods, not just for the base mod. [#684](https://github.com/clusterio/clusterio/pull/684)
|
||||
- Added option for silent testing which only shows the results, can be ran using `pnpm silent-test` [#680](https://github.com/clusterio/clusterio/pull/680)
|
||||
- Added prompt for http port on installation of Clusterio. [#564](https://github.com/clusterio/clusterio/issues/564)
|
||||
- Added drag and drop overlay to upload button on mods page and save table on instance page. [#591](https://github.com/clusterio/clusterio/issues/591)
|
||||
- Automatically add plugins from ./plugins, ./external_plugins and installed npm modules. [#692](https://github.com/clusterio/clusterio/pull/692)
|
||||
- `compat.table_to_json` and `compat.json_to_table` supported in all stages in pre 2.0 [#687](https://github.com/clusterio/clusterio/issues/687)
|
||||
- Add "connect to server" button to web interface, launches with steam api. [#608](https://github.com/clusterio/clusterio/issues/608)
|
||||
- Add space platform name mapping to space platform statistics. [#706](https://github.com/clusterio/clusterio/pull/706)
|
||||
- Track space platform speed metric clusterio_statistics_exporter_platform_speed. [#706](https://github.com/clusterio/clusterio/pull/706)
|
||||
- Track space platform weight metric clusterio_statistics_exporter_platform_weight. [#706](https://github.com/clusterio/clusterio/pull/706)
|
||||
- Added bidirectional sync to admin and ban list. [#709](https://github.com/clusterio/clusterio/pull/709)
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fixed IPC errors reporting undefined when no stack trace was present. [#624](https://github.com/clusterio/clusterio/pull/624)
|
||||
- Fixed loopback routing for instances reporting the wrong error message. [#625](https://github.com/clusterio/clusterio/pull/625)
|
||||
- Fixed docs linking to non-existing js files after ts migration. [#633](https://github.com/clusterio/clusterio/pull/633)
|
||||
- Fixed CI workflows using deprecated version of nodejs. [#637](https://github.com/clusterio/clusterio/pull/637)
|
||||
- Fixed linux only tests to run on windows. [#661](https://github.com/clusterio/clusterio/pull/661)
|
||||
- Fixed log events being sent from a host over an invalid websocket. [#639](https://github.com/clusterio/clusterio/pull/639)
|
||||
- Fixed spaces passed in arguments to the installer causing it to break. [#620](https://github.com/clusterio/clusterio/issues/620)
|
||||
- Fixed numeric admin name breaking login. [#536](https://github.com/clusterio/clusterio/issues/536)
|
||||
- Fixed issues with subscriptions which prevented them from working on instances or generic events. [#655](https://github.com/clusterio/clusterio/pull/655).
|
||||
- Fixed unreliable datastore tests. [#662](https://github.com/clusterio/clusterio/pull/662).
|
||||
- Fixed 2.0 version extraction from linux headless. downloaded during installation of clusterio. [#671](https://github.com/clusterio/clusterio/pull/671)
|
||||
- Fixed invalid transient state during server start. [#676](https://github.com/clusterio/clusterio/issues/676)
|
||||
- Fixed host user sync to instances after reconnecting to the. controller. [#678](https://github.com/clusterio/clusterio/pull/678)
|
||||
- Fixed WebUI adding and displaying the wrong base mod version. [#684](https://github.com/clusterio/clusterio/pull/684)
|
||||
- Fixed WebUI inconsistent pattern matching for factorio version. [#684](https://github.com/clusterio/clusterio/pull/684)
|
||||
- Fixed `safeOutputFile` not being atomic because of a missing path separator. [#659](https://github.com/clusterio/clusterio/pull/659)
|
||||
- Fixed built-in mods showing as missing in the WebUI. [#581](https://github.com/clusterio/clusterio/issues/581)
|
||||
- Fixed dirty flag not being set when only the config of an instance is updated. [#675](https://github.com/clusterio/clusterio/pull/675)
|
||||
- Fixed incorrect command being used to remove players from the whitelist. [#679](https://github.com/clusterio/clusterio/pull/679)
|
||||
- Fixed CI following undocumented breaking change from downstream dependency. [#683](https://github.com/clusterio/clusterio/pull/683)
|
||||
- Fixed instance crash when missing saves folder. [#698](https://github.com/clusterio/clusterio/issues/698)
|
||||
- Fixed statistics exporter not working on newer versions of windows 11 due to wmic deprecation. [#707](https://github.com/clusterio/clusterio/issues/707)
|
||||
|
||||
### Changes
|
||||
|
||||
- Added prompt for the HTTP port to host the controller to the installer. [#619](https://github.com/clusterio/clusterio/pull/619)
|
||||
- Added tests for sendEvent and sendRequest throwing the correct error messages. [#638](https://github.com/clusterio/clusterio/pull/638)
|
||||
- Updated all web ui pages to use correct page header component. [#643](https://github.com/clusterio/clusterio/pull/643)
|
||||
- Bumped Typescript version to 5.5 and implemented `${configDir}` in base configs for "outDir". [#648](https://github.com/clusterio/clusterio/pull/648).
|
||||
- Added factorio.executable_path option which allows overriding the default path to the Factorio executable run.
|
||||
- Added instance version display. [#573](https://github.com/clusterio/clusterio/pull/573)
|
||||
- Added `factorio.shutdown_timeout` config to set the time the host will wait for a Factorio server to shut down befor killing it.
|
||||
- Changed shutdown logic to prefer sending a /quit command via RCON instead of using diverging logic on Windows and Linux.
|
||||
- Added `controller.factorio_username` and `controller.factorio_token` config to set Factorio credentials used for the whole cluster.
|
||||
- Added `controller.share_factorio_credential_with_hosts` config to optionally require hosts to provide their own credentials.
|
||||
- Added `host.factorio_username` and `host.factorio_token` config to set Factorio credentials used on a given host.
|
||||
- Changed `instance.rcon_password` config to be a credential field that can not be read by the control interface.
|
||||
- Changed `host.id` `instance.id` and `instance.assign_host` config to be hidden fields that can not be seen on the web ui.
|
||||
- Updated display name and description of `controller.external_address` to avoid confusion. [#674](https://github.com/clusterio/clusterio/pull/674)
|
||||
- Renamed config value `controller.external_address` to `controller.public_url`, migrations are applied automatically.
|
||||
- String config values with whitelisted values (enum) now display as a selection dropdown. [#701](https://github.com/clusterio/clusterio/pull/701)
|
||||
- Existing selection dropdowns now support search filtering. [#701](https://github.com/clusterio/clusterio/pull/701)
|
||||
- Added surface label to clusterio_statistics_exporter_instance_force_flow_statistics. [#691](https://github.com/clusterio/clusterio/issues/691)
|
||||
- Compat lib now supports versions from 0.17 to 2.0 [#711](https://github.com/clusterio/clusterio/pull/711)
|
||||
- Add bidirectional sync to admin and ban list. [#709](https://github.com/clusterio/clusterio/pull/709)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Previously silent errors for controller.sendEvent now throw exceptions [#625](https://github.com/clusterio/clusterio/pull/625).
|
||||
- @clusterio/controller export InstanceInfo has added factorioVersion parameter.
|
||||
- Argument order for `Config.canAccess` changed to require an access mode passed as the second argument.
|
||||
- Instances can now have `factorio.enable_script_commands` disabled which will throw an error when any script command is used over rcon. [#681](https://github.com/clusterio/clusterio/pull/681).
|
||||
- User IDs are now case insensitive, duplicate users will be automatically merged with a backup created. [#682](https://github.com/clusterio/clusterio/pull/682)
|
||||
- Plugins now require the keyword "clusterio-plugin" in package.json to be automatically loaded [#692](https://github.com/clusterio/clusterio/pull/692)
|
||||
|
||||
Many thanks to the following for contributing to this release:
|
||||
[@CCpersonguy](https://github.com/CCpersonguy)
|
||||
[@Cooldude2606](https://github.com/Cooldude2606)
|
||||
[@Danielv123](https://github.com/Danielv123)
|
||||
[@Hornwitser](https://github.com/Hornwitser)
|
||||
[@Laar](https://github.com/Laar)
|
||||
[@psihius](https://github.com/psihius)
|
||||
|
||||
|
||||
## Version 2.0.0-alpha.18
|
||||
|
||||
### Features
|
||||
|
||||
- Added logging of HTTP requests and errors on the controller [#556](https://github.com/clusterio/clusterio/pull/556).
|
||||
- Added caching of data parsed from mods to speed up startup time [#606](https://github.com/clusterio/clusterio/pull/606).
|
||||
- Added command to installer to generate the code for making a new plugin [#570](https://github.com/clusterio/clusterio/pull/570).
|
||||
|
||||
### Changes
|
||||
|
||||
- Fixed Inventory Sync reporting a negative database size [#598](https://github.com/clusterio/clusterio/pull/598).
|
||||
- Fixed plugin events being invoked while a host connection was in an invalid state.
|
||||
- Suppresed bogus warning about event listener leak in the browser's console [#600](https://github.com/clusterio/clusterio/pull/600).
|
||||
- Fixed installer breaking on Windows after Node.js released a security fix [#614](https://github.com/clusterio/clusterio/pull/614).
|
||||
- During a data export the mod settings in mod packs will now be corrected to the type of the settings prototype if the type is incorrect.
|
||||
- Fixed missing color setting causing the Mod Pack view in the Web UI to show an error [#609](https://github.com/clusterio/clusterio/issues/609).
|
||||
|
||||
Many thanks to the following for contributing to this release:
|
||||
[@Cooldude2606](https://github.com/Cooldude2606)
|
||||
[@Danielv123](https://github.com/Danielv123)
|
||||
[@Hornwitser](https://github.com/Hornwitser)
|
||||
|
||||
|
||||
## Version 2.0.0-alpha.17
|
||||
|
||||
### Features
|
||||
|
||||
- Made the host an instance is assigned to link to the host page for that host in the Web UI [#587](https://github.com/clusterio/clusterio/pull/587).
|
||||
- Added pagination to the saves list.
|
||||
|
||||
### Changes
|
||||
|
||||
- Fixed an unhandled error in Subspace Storage crashing the host.
|
||||
- Fixed an unhandled rejection in disconnect logic crashing the host.
|
||||
- Fixed instance screen in Web UI not updating when an instance was reassigned.
|
||||
- Fixed an error thrown in the rendering of one page in the Web UI causing all pages to become inaccessible.
|
||||
- Fixed Web UI not showing instance saves when the permission to list and subscribe the saves is granted but the user is not a cluster admin.
|
||||
- Similarly fixed Web UI not showing the mod pack create, delete, list and host revoke token actions if the user is not a cluster admin.
|
||||
- Fixed Research Sync applying progress of a previous techology level to the level on the controller when it's the active research during instance startup.
|
||||
- Fixed data export with a mod using shorthand notation for the default value of a color-setting causing the ModPack data to be set to invalid data.
|
||||
- Fixed per user per instance player stats not being deleted when the instance it's for is deleted.
|
||||
- Fixed mods setting the item prototype icons property to an object mapping instead of an array breaking the data export.
|
||||
- Fixed a bad mod in the host's mods folder causing instance start to fail instead of the host re-downloading the mod.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Renamed the permission `core.instance.save.list.subscribe` to `core.instance.save.subscribe`.
|
||||
|
||||
Many thanks to the following for contributing to this release:
|
||||
[@Cooldude2606](https://github.com/Cooldude2606)
|
||||
[@Danielv123](https://github.com/Danielv123)
|
||||
[@Hornwitser](https://github.com/Hornwitser)
|
||||
|
||||
|
||||
## Version 2.0.0-alpha.16
|
||||
|
||||
### Changes
|
||||
|
||||
- Fixed message sequences not being renumbered when forwarded between links causing reconnects to send duplicated messages.
|
||||
|
||||
|
||||
## Version 2.0.0-alpha.15
|
||||
|
||||
### Features
|
||||
|
||||
- Added support for `color-settings` Mod settings [#568](https://github.com/clusterio/clusterio/issues/568).
|
||||
- Added `clusterio_controller_pending_requests` and `clusterio_host_pending_requests` metrics.
|
||||
|
||||
### Changes
|
||||
|
||||
- Fixed generated systemd file for clusteriohost not enabling source maps.
|
||||
- Fixed ctl not exporting types used for adding commands to ctl plugins.
|
||||
- Fixed controller only plugins throwing an error about missing a config field on startup out when added to hosts.
|
||||
- Fixed installer incorrectly claiming the options --factorio-dir and --no-download-headless conflicts with each other.
|
||||
- Fixed a memory leak on every request sent over the WebSocket.
|
||||
- Fixed assertion error on closing or invalidating a link with messages forwarded from a virtual link.
|
||||
- Fixed host hanging if stopped while an instance is in the process of creating a save during startup.
|
||||
- Fixed `instance.auto_start` option.
|
||||
- Fixed host log spammed with errors when plugins broadcast messages to instances with the plugin disabled.
|
||||
- Fixed properties of object config fields not being possible to remove.
|
||||
|
||||
|
||||
## Version 2.0.0-alpha.14
|
||||
|
||||
This release does not retain backwards compatibility with 2.0.0-alpha.13 meaning all parts of the cluster needs to updated at the same time.
|
||||
See the [migration guide](/docs/migrating-from-2.0.0-alpha.13.md) for how to update.
|
||||
|
||||
### Major Features
|
||||
|
||||
- Added mod management. Mods can be uploaded to the controller and included in mod packs that can be assigned to instances and run.
|
||||
- Added host port ranges which the instance ports will be automatically assigned a port from if factorio.game_port is not set.
|
||||
- Added system info such as CPU model, hostname and kernel along with basic metrics for CPU usage, memory usage and disk usage for the controller and hosts.
|
||||
- Added ability to remotely view and modify the configuration of hosts.
|
||||
|
||||
### Features
|
||||
|
||||
- Added logging of Clusterio version on startup
|
||||
- Added `clusterio_controller_log_bytes` metric counting the size of the log files on the controller.
|
||||
- Added warning logged when plugins on a connecting host does not match the ones installed on the controller.
|
||||
- Added ability to revoke user tokens.
|
||||
- Added restart required indicator to config fields in the Web UI.
|
||||
- Added ability to revoke host tokens.
|
||||
- Added support for plugins to run in the context of a host.
|
||||
- Added more feedback when the web UI and or its plugins fails to load.
|
||||
- Added autosaving of data held in memory on the controller such as configs, instances, users, mods to prevent large data loss on crashing.
|
||||
- Added `system_info` metric with kernel, instruction set, CPU Model, hostname and Node.js version of the controller.
|
||||
- Added `clusterio_host_system_info` metric with kernel, instruction set, CPU Model, hostname and Node.js version of hosts.
|
||||
- Added `system_cpu_seconds_total`, `system_cpu_idle_seconds_total`, `system_memory_capacity_bytes`, `system_memory_available_bytes`, `system_disk_capacity_bytes`, `system_disk_available_bytes` metrics for CPU, memory and disk usage on the controller.
|
||||
- Added `clusterio_host_system_cpu_seconds_total`, `clusterio_host_system_cpu_idle_seconds_total`, `clusterio_host_system_memory_capacity_bytes`, `clusterio_host_system_memory_available_bytes`, `clusterio_host_system_disk_capacity_bytes`, `clusterio_host_system_disk_available_bytes` metrics for CPU, memory and disk usage on hosts.
|
||||
- Added support for reading the client's IP address from the `X-Forwarded-For` header when the source address is in the `controller.trusted_proxies` config option.
|
||||
- Added field showing the IP address a host is connecting from.
|
||||
- Added per player statistic tracking when they were first seen joining a particular server.
|
||||
- Added ability to remotely restart and/or stop the controller and hosts.
|
||||
|
||||
### Changes
|
||||
|
||||
- Fixed filenames with extensions that are invalid on Windows such as "con.zip" not being treated as invalid.
|
||||
- Fixed generate host token modal not using a random id when the optional id field is unset.
|
||||
- Exported Factorio data is now tied to mod packs instead of being per cluster.
|
||||
- Fixed export failing if a mod uses a different root folder mod_version in its zip file.
|
||||
- Fixed sorting of user last seen column in the Web UI.
|
||||
- The list users permission is now granted by default to players.
|
||||
- Fixed incorrect handling of hosts and instance with an id of 0 causing them to not update properly.
|
||||
- Fixed unable to unassign instances in the Web UI.
|
||||
- Fixed host crashing if the Factorio server is not marked executable.
|
||||
- Fixed errors logged from instance not being attributed to the instance in certain situations.
|
||||
- Hosts no longer print messages from instances into their console.
|
||||
- Clusterio now treat unhandled Node.js errors and promise rejections as fatal and exit immediately.
|
||||
- Fixed a potential for data loss after a hard system crash due to fsync not being called on files written.
|
||||
- Fixed Factorio server crashing in the research_sync plugin when a mod enables a previously disabled technology and a player attempts to research it.
|
||||
- Fixed Factorio server crashing when the game starts with tick_paused = true
|
||||
- Removed migration of alpha 10's single file log format.
|
||||
- Log errors linking mods on Windows instead of preventing startup.
|
||||
- Added locale support for modules patched into to the game save.
|
||||
- Added support for defining a module_exports.lua file for modules patched into the game save.
|
||||
- Fixed controller logs being split on server's local time day boundary instead of UTC day boundary.
|
||||
- Fixed Unicode BOM in mod locale files breaking instance data export.
|
||||
- Added a warning to the server-setting.json that is automatically written to instance folders stating changes will be overwritten.
|
||||
- Changed save patching to use a `.tmp.zip` extension for the temporary file created instead of `zip.tmp`.
|
||||
- Fixed Inventory Sync wiping the player's inventory when logging out while in the Space Exploration remote view mode.
|
||||
- Fixed the public /api/plugins endponit leaking filesystem paths on the controller.
|
||||
- Refactored list subscriptions in the Web UI to cache and only fetch updated entries when navigating to previously seen list.
|
||||
- Added caching of save lists on the controller, enabling the saves of instances on offline hosts to still be seen.
|
||||
- Hosts in the assign instance dropdown is now sorted by name.
|
||||
- Added `non_blocking_saving` as one of the default settings added to `factorio.settings` on new instances.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Renamed usage of the terms master and slave to controller and host in all places they occur.
|
||||
- Mods located in `sharedMods` are no longer copied to instances' mods folder.
|
||||
- The instances' mods folder is now managed through mod packs, mods not part of the mod pack will be automatically deleted.
|
||||
- Removed ability to import sub packages like `@clusterio/lib/link` from.
|
||||
- Replaced `clusterio_controller_http_hits_total` metric with `clusterio_controller_http_endpoint_duration_seconds`.
|
||||
- Replaced `clusterio_controller_last_query_log_duration_seconds` metric with `clusterio_controller_query_log_duration_seconds` summary.
|
||||
- Reworked the network protocol to be simpler to work and reason with.
|
||||
- Bumped minimum supported Node.js version to v18.
|
||||
- Ported the codebase to TypeScript
|
||||
- Changed the config format stored on disk to be much simpler.
|
||||
|
||||
### Contributors
|
||||
|
||||
Many thanks to the following for contributing code in this release:
|
||||
[@Hornwitser](https://github.com/Hornwitser) [@Luciole0x](https://github.com/Luciole0x) [@Cooldude2606](https://github.com/Cooldude2606) [@Danielv123](https://github.com/Danielv123) [@Reavershark](https://github.com/Reavershark) [@Laar](https://github.com/Laar) [@rstrom1763](https://github.com/rstrom1763)
|
||||
|
||||
## 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 hosts.
|
||||
Hosts 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 controller that can be accessed through the clusterioctl cli tool.
|
||||
- Rewritten the communication between hosts and the controller from scratch.
|
||||
The new system is based on a WebSocket connection between the hosts and the controller 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.
|
||||
- Added centralized logging for the cluster.
|
||||
All logs from both Clusterio and Factorio is stored in a shared log which can be inspected and queried.
|
||||
- Added remote save management with the ability to list, create, rename, copy, upload, download, transfer, delete and load saves for instances.
|
||||
|
||||
### Features
|
||||
|
||||
- Added export of pollution statitics.
|
||||
- Reconnection logic that esures no data is dropped talking to the controller 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 controller 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.
|
||||
- Added stripping of long paths in the Factorio server log.
|
||||
- Added option to configure maximum number of commands to send in parallel.
|
||||
- Added option to auto start instances on host startup.
|
||||
- Added metric mapping of host and instance ids to their names, allowing them to be displayed by name in queries that join with the mapping.
|
||||
- Added option to configure timeout for exported Prometheus metrics.
|
||||
- Added option to set address to bind HTTP(S) port to.
|
||||
- Added an overview of the installed plugins to the web interface.
|
||||
- Added clusterioctl command to list plugins on the controller.
|
||||
- Added Player Auth plugin for logging in to the web interface by proving the ability to log in to a server as a given user.
|
||||
- Added Node.js based installer to simplify setting up a cluster.
|
||||
|
||||
### Changes
|
||||
|
||||
- Added error handling during controller 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 host, creating a save for an instance and starting an instance is now four separate commands.
|
||||
- Removed oddball limits to hosts.json size.
|
||||
- Moved host 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.
|
||||
- Controller now defaults to hosting on https on port 8443.
|
||||
- Renamed client to slave.
|
||||
- Removed rotation of factorio-current.log files.
|
||||
|
||||
### 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.
|
||||
- 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 hosts in the host 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/hosts.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 controller managing hosts/instances.
|
||||
- Removed `--databaseDirectory`, `--controllerPort`, and `--sslPort` command line arguments from the controller.
|
||||
- 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.
|
||||
- Controller no longer creates secret-api-token.txt.
|
||||
- Removed automatic creation of self-signed TLS certificate.
|
||||
- Node.js versions below 12 are no longer supported.
|
||||
|
||||
## 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/clusterio/issues/217
|
||||
[#229]: https://github.com/clusterio/clusterio/issues/229
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Added authentication to the socket.io server running on the master.
|
||||
@@ -1,58 +1,23 @@
|
||||
FROM node:20 as subspace_storage_builder
|
||||
RUN apt update && apt install -y git
|
||||
WORKDIR /
|
||||
RUN git clone https://github.com/clusterio/subspace_storage.git
|
||||
WORKDIR /subspace_storage
|
||||
RUN npm install \
|
||||
&& node build
|
||||
FROM node:9.11.2
|
||||
RUN apt-get update && apt install git curl tar -y
|
||||
RUN mkdir factorioClusterio
|
||||
|
||||
FROM node:20 as clusterio_builder
|
||||
RUN apt update \
|
||||
&& apt install -y wget \
|
||||
&& mkdir /clusterio
|
||||
WORKDIR /clusterio
|
||||
RUN wget -q -O factorio.tar.gz https://www.factorio.com/get-download/latest/headless/linux64 && tar -xf factorio.tar.gz && rm factorio.tar.gz
|
||||
# Copy project files into the container
|
||||
COPY . .
|
||||
#RUN git clone -b master https://github.com/Danielv123/factorioClusterio.git && cd factorioClusterio && npm install
|
||||
COPY . /factorioClusterio
|
||||
RUN cd /factorioClusterio && npm install
|
||||
RUN cd /factorioClusterio && curl -o factorio.tar.gz -L https://www.factorio.com/get-download/latest/headless/linux64 && tar -xf factorio.tar.gz && rm factorio.tar.gz
|
||||
|
||||
RUN pnpm install
|
||||
WORKDIR /factorioClusterio
|
||||
RUN mkdir instances sharedMods
|
||||
RUN cp config.json.dist config.json
|
||||
|
||||
# Install plugins. This is intended as a reasonable default, enabling plugins to make for fun gameplay.
|
||||
# If you want a different set of plugins, consider using this as the base image for your own.
|
||||
#RUN pnpm install @clusterio/plugin-subspace_storage
|
||||
#RUN npx clusteriocontroller plugin add @clusterio/plugin-subspace_storage
|
||||
RUN node client.js download
|
||||
|
||||
COPY --from=subspace_storage_builder /subspace_storage/dist/ /clusterio/mods/
|
||||
LABEL maintainer "danielv@live.no"
|
||||
|
||||
# Build Lua library mod
|
||||
RUN node packages/lib/build_mod --build --source-dir packages/host/lua/clusterio_lib \
|
||||
&& mv dist/* mods/ \
|
||||
&& mkdir temp \
|
||||
&& mkdir temp/test \
|
||||
&& cp mods/ temp/test/ -r \
|
||||
&& ls mods
|
||||
EXPOSE 8080 34167
|
||||
VOLUME /factorioClusterio/instances
|
||||
VOLUME /factorioClusterio/sharedMods
|
||||
VOLUME /factorioClusterio/sharedPlugins
|
||||
|
||||
# Remove node_modules
|
||||
RUN find . -name 'node_modules' -type d -prune -print -exec rm -rf '{}' \;
|
||||
|
||||
FROM frolvlad/alpine-glibc AS clusterio_final
|
||||
|
||||
RUN apk add --update bash nodejs npm
|
||||
|
||||
COPY --from=clusterio_builder /clusterio /clusterio
|
||||
WORKDIR /clusterio
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN pnpm install --production
|
||||
LABEL maintainer "danielv@danielv.no"
|
||||
|
||||
FROM frolvlad/alpine-glibc AS clusterio_testing
|
||||
|
||||
RUN apk add --update bash nodejs npm
|
||||
|
||||
COPY --from=clusterio_builder /clusterio /clusterio
|
||||
WORKDIR /clusterio
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN pnpm install
|
||||
RUN pnpm install chalk semver
|
||||
CMD RCONPORT="$RCONPORT" FACTORIOPORT="34167" MODE="$MODE" node $MODE\.js start $INSTANCE
|
||||
@@ -19,3 +19,8 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
The icons for angels modded items located in /static/pictures/ are licensed
|
||||
by https://mods.factorio.com/user/Arch666Angel under CC BY-NC-ND 4.0
|
||||
https://creativecommons.org/licenses/by-nc-nd/4.0/
|
||||
|
||||
@@ -4,309 +4,243 @@
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
# Clusterio
|
||||
# factorioClusterio
|
||||
|
||||
Discord for development/support/play: https://discord.gg/5XuDkje
|
||||
|
||||
## Important notice
|
||||
|
||||
Clusterio 2.0 is still in alpha, however the previous stable has been abandoned and is no longer supported.
|
||||
Despite being alpha it's reasonably stable now and there's no major breakages expected before a stable release of 2.0.
|
||||
If you are starting a new cluster, it's highly recommended to use the 2.0 alpha.
|
||||
|
||||
|
||||
### Ways to support me/the project:
|
||||
|
||||
* Contribute with code/documentation.
|
||||
See [Contributing](docs/contributing.md) for how to make pull requests.
|
||||
Always nice to move the project forward.
|
||||
* Contribute with code/documentation. Always nice to move the project forward
|
||||
|
||||
* Support me monetarily on [patreon](https://www.patreon.com/danielv123) or paypal: danielv@live.no
|
||||
|
||||
### Table of contents
|
||||
|
||||
* [Introduction](#introduction)
|
||||
* [Features](#features)
|
||||
* [Plugins](#plugins)
|
||||
* [Installation](#installation)
|
||||
* [Ubuntu setup](#ubuntu-setup)
|
||||
* [Windows setup](#windows-setup)
|
||||
* [MacOS setup](#macos-setup)
|
||||
* [Installing Plugins](#installing-plugins)
|
||||
* [Configure Controller](#configure-controller)
|
||||
* [Managing Factorio mods](#managing-factorio-mods)
|
||||
* [Setting up shared storage](#setting-up-shared-storage)
|
||||
* [Running Clusterio](#running-clusterio)
|
||||
* [Setting up remote hosts](#setting-up-remote-hosts)
|
||||
* [Setting up clusterioctl](#setting-up-clusterioctl)
|
||||
* [Known issues](#known-issues)
|
||||
* [Introduction & methodology](#introduction)
|
||||
|
||||
* [Ubuntu setup](#ubuntu-setup)
|
||||
|
||||
* [Windows setup](#windows-setup)
|
||||
|
||||
* [Optional plugins](#Plugins)
|
||||
|
||||
* [Common problems](#Common-problems)
|
||||
|
||||
* [Command cheatsheet](#cheatsheet)
|
||||
|
||||
## Introduction
|
||||
|
||||
Clusterio is a clustered Factorio server manager that provides the tooling for implementing cross server interactions in Factorio.
|
||||
It was previously best known for implementing cross server transfer and cloud storage of items via teleporter chests.
|
||||
But this functionality has been pulled out of Clusterio into its own plugin for Clusterio named [Subspace Storage](https://github.com/clusterio/subspace_storage).
|
||||
Features:
|
||||
|
||||
By itself Clusterio doesn't change the gameplay in any way, you could even use Clusterio to manage completely vanilla Factorio servers.
|
||||
Plugins do the work of modding in the visible changes into the game, see the [Plugins section](#plugins) for ready-made plugins you can install into a Clusterio cluster.
|
||||
- Entities to send/recieve items
|
||||
|
||||
- Cross dimensional storage
|
||||
|
||||
- Sending of liquids
|
||||
|
||||
- Sending of circuit network signals
|
||||
|
||||
- Inventory combinator to display item levels in the cluster (and epoch time)
|
||||
|
||||
- Reporting of graphs and UPS on master interface (Also has extensive Prometheus reporting)
|
||||
|
||||
Optional extras (see [Plugins](#Plugins))
|
||||
|
||||
- Have your inventory synchronize across servers
|
||||
|
||||
- Teleport trans from the border of one world to te next
|
||||
|
||||
- Show in-game chat in discord
|
||||
|
||||
|
||||
## Features
|
||||
Connection diagram:
|
||||
|
||||
- Clustered Factorio server management allowing you manage the running of Factorio servers across a fleet of physical servers from both a web interface and a command line interface.
|
||||

|
||||
|
||||
- User list management for synchronizing in-game admins, whitelisted users, and bans to all the servers in the cluster.
|
||||
There can be any number of clients connected to each slave, and any number of slaves connected to a master but there can only be one master server in each cluster.
|
||||
|
||||
- Integrated support for exporting statistics for the whole cluster to Prometheus via a single metrics endpoint.
|
||||
**How does it work?**
|
||||
|
||||
- Extensive plugin support for adding your own cross server features to Factorio using Clusterio's communication backbone.
|
||||
Traditional factorio mods have always been limited by the games deterministic design. This gives us a very bug free and predictable game, but doesn't allow us cool stuff such as internet communication.
|
||||
Clusterio exploits one of the games logging features, game.write_file and RCON to communicate between servers. Sending an item from one server to another takes this path:
|
||||
|
||||
1. server1: Chest has stuff in it, write the contents to a file and delete them from the game world
|
||||
|
||||
2. client.js on server1: File has stuff in it, parse and send to the master for storage
|
||||
|
||||
3. master: server1 gave us stuff, store it in the storage and write some statistics
|
||||
|
||||
4. server2: get-chest is empty, write a request to file
|
||||
|
||||
5. client.js on server2: Request file has stuff in it, parse and send a request to master for more items of that type
|
||||
|
||||
6. master: server2 asked for stuff, check if we have enough and how much demand there is, then send however much is appropriate back
|
||||
|
||||
7. client.js on server2: We were allowed to import x of item y, run command /c remote.call("clusterio", "importMany", "{'copper-plate':120}")
|
||||
|
||||
This process works the same for both items and liquids, independant on what mods are used. Yes, modded items are fully supported.
|
||||
|
||||
Clusterio can also do a few other neat things, such as giving you access to epoch time, syncing player inventories between servers, keeping track of playtime (playerManager plugin), teleporting trains between servers (trainTeleports) and exporting tons of factorio related statistics to Prometheus for graphing in grafana.
|
||||
|
||||
## Ubuntu setup
|
||||
|
||||
NodeJS does not support EOL ubuntu releases. Make sure you are on the most recent LTS release or newer.
|
||||
|
||||
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
|
||||
tar -xf factorio.tar.gz
|
||||
npm install
|
||||
cp config.json.dist config.json
|
||||
node ./lib/npmPostinstall.js
|
||||
|
||||
|
||||
## Plugins
|
||||
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.
|
||||
|
||||
The heart of Clusterio is its plugin system.
|
||||
Plugins add functionality to Factorio servers, Clusterio itself or both.
|
||||
These are the plugins supported and maintained by the Clusterio developers:
|
||||
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.
|
||||
|
||||
- [Global Chat](/plugins/global_chat/README.md): share the in-game chat between servers.
|
||||
- [Research Sync](/plugins/research_sync/README.md): synchronize research progress and technologies unlocked between servers.
|
||||
- [Statistics Exporter](/plugins/statistics_exporter/README.md): collect in-game statistics from all the servers and makes it available to the Prometheus endpoint on the controller.
|
||||
- [Subspace Storage](https://github.com/clusterio/subspace_storage): Provide shared storage that can transport items between servers via teleport chests.
|
||||
- [Player Auth](/plugins/player_auth/README.md): Provides authentication to the cluster via logging into a Factorio server.
|
||||
- [Inventory Sync](/plugins/inventory_sync/README.md): Synchronizes your inventory between servers.
|
||||
* You get the `masterAuthToken` from `secret-api-token.txt` in the master install dir after running the master twice.
|
||||
|
||||
There's also plugins developed and maintained by the community:
|
||||
* You get your factorio matchmaking token from factorio.com
|
||||
|
||||
- [Discord Bridge](https://github.com/Hornwitser/discord_bridge) (@hornwitser/discord_bridge): Bridges chat between instances and Discord.
|
||||
By Hornwitser.
|
||||
- [Server Select](https://github.com/Hornwitser/server_select/tree/clusterio-2.0) (@hornwitser/server_select): In-game GUI for connecting to other servers in the cluster.
|
||||
Originally by Godmave, ported to 2.0 by Hornwitser.
|
||||
* The `masterAuthSecret` should never be touched unless you want to invalidate everyones authentication tokens
|
||||
|
||||
Want to make your own plugin?
|
||||
Check out the documentation on [Writing Plugins](/docs/writing-plugins.md) for where to start.
|
||||
**Master**
|
||||
|
||||
pm2 start master.js --name master
|
||||
|
||||
## Installation
|
||||
OR
|
||||
|
||||
Clusterio runs on all LTS version of NodeJS, it is distributed via npm and comes with a guided install script.
|
||||
If you already have a recent Node.js installed, you can set it up in a new directory with:
|
||||
node master.js
|
||||
|
||||
npm init "@clusterio"
|
||||
**Server Host**
|
||||
|
||||
Otherwise see below for OS specific instructions.
|
||||
Check out all our of decisions for [node, factorio, and operation support](/docs/decisions.md#supportability).
|
||||
To download the mod for all its non vanilla features and items, (optional, but very recommended)
|
||||
|
||||
### Ubuntu setup
|
||||
node client.js manage shared mods add clusterio
|
||||
|
||||
1. Install Node.js v18 or higher.
|
||||
Note the version of Node.js provided by the Ubuntu/debian repos are too old and you will have to use the nodesource PPA
|
||||
To create a new instance (its own save, set of mods and config files)
|
||||
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - &&\
|
||||
sudo apt-get install -y nodejs
|
||||
node client.js start [instancename]
|
||||
|
||||
2. Log in as the user you wish to install Clusterio as (do not use root), and then create a new directory and run the Clusterio installer:
|
||||
To launch an instance with pm2
|
||||
|
||||
mkdir clusterio
|
||||
cd clusterio
|
||||
npm init "@clusterio"
|
||||
|
||||
Make sure to note down the admin authentication token it provides at the end as you will need it later.
|
||||
|
||||
3. Optionally copy the generated systemd service files in `systemd` folder to `/etc/systemd/system/`.
|
||||
pm2 start --name [instancename] client.js -- start [instancename]
|
||||
|
||||
use `nano config.json` to change settings.
|
||||
|
||||
**Ubuntu with Docker**
|
||||
|
||||
The Docker support for Clusterio is currently 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 clusterio
|
||||
sudo docker build -t clusterio --no-cache --force-rm factorioClusterio
|
||||
|
||||
sudo docker run --name controller -e MODE=controller -p 1234:8080 -d -it --restart=unless-stopped danielvestol/clusterio
|
||||
sudo docker run --name master -e MODE=master -p 1234:8080 -d -it --restart=unless-stopped danielvestol/clusterio
|
||||
|
||||
sudo docker run --name host -e MODE=host -e INSTANCE=world1 -v /srv/clusterio/instances:/clusterio/instances -p 1235:34167 -it --restart=unless-stopped danielvestol/clusterio
|
||||
sudo docker run --name slave -e MODE=client -e INSTANCE=world1 -v /srv/clusterio/instances:/factorioClusterio/instances -p 1235:34167 -it --restart=unless-stopped danielvestol/clusterio
|
||||
|
||||
The -v flag is used to specify the instance directory.
|
||||
Your instances (save files etc) will be stored there.
|
||||
-->
|
||||
The -v flag is used to specify the instance directory. Your instances (save files etc) will be stored there.
|
||||
|
||||
### Windows setup
|
||||
## Windows setup
|
||||
|
||||
1. Download and install the latest LTS release from http://nodejs.org.
|
||||
Clusterio is built up of multiple parts. Here is a quick guide:
|
||||
|
||||
2. Create a new empty directory for the installation and navigate into it.
|
||||
Open a PowerShell window in this new directory by Shift+Right clicking inside it and choosing "Open PowerShell window here" and then run the following command.
|
||||
Master = master.js
|
||||
|
||||
npm init "@clusterio"
|
||||
Server host (Slave) = client.js + factorio server
|
||||
|
||||
Make sure to note down the admin authentication token it provides at the end as you will need it later.
|
||||
Game Client = The people connecting to the server
|
||||
|
||||
3. If you chose to use local factorio directory for the Factorio installation, download the Windows 64-bit zip package from https://www.factorio.com/download and extract it to the `factorio` folder in your clusterio installation folder.
|
||||
**Requirements**
|
||||
|
||||
download and install nodeJS 8 or 9 from http://nodejs.org
|
||||
|
||||
### MacOS Setup
|
||||
download and install git from https://git-scm.com/
|
||||
|
||||
1. Install the latest Node.js LTS release from http://nodejs.org or use brew (`brew install node`).
|
||||
reboot when you are done, then proceed to the next steps. *reboots matter*
|
||||
|
||||
2. Open Terminal or Command prompt in the directory you want to install to and run the following commands.
|
||||
**Master**
|
||||
|
||||
mkdir clusterio
|
||||
cd clusterio
|
||||
npm init "@clusterio"
|
||||
1. download and run https://puu.sh/toFHl/01eebbb333.bat
|
||||
|
||||
Make sure to note down the admin authentication token it provides at the end as you will need it later.
|
||||
2. Copy config.json.dist to config.json
|
||||
|
||||
3. If you chose to use local factorio directory for the Factorio installation, you will need to obtain and copy a mac version of Factorio and unpack it to the `factorio` folder in your clusterio installation folder.
|
||||
3. Follow the instructions in the bat file
|
||||
3. Some of the instructions are outdated. If you get stuck somewhere, look at the Ubuntu section.
|
||||
|
||||
4. Run `node client.js manage shared mods add clusterio`
|
||||
|
||||
### Installing Plugins
|
||||
5. type `node master.js` to start the server
|
||||
|
||||
For well-known plugins you can select them during installation and no further steps are necessary to make them work.
|
||||
Installing plugins that are not offered by the installer consists of two steps.
|
||||
First install the package the plugin is provided by via npm, for example
|
||||
**Server Host**
|
||||
|
||||
npm install @clusterio/plugin-subspace_storage
|
||||
1. download and run https://puu.sh/toFHl/01eebbb333.bat
|
||||
|
||||
Then tell clusterio that this plugin exists by adding it as a plugin.
|
||||
2. Copy config.json.dist to config.json
|
||||
|
||||
npx clusteriocontroller plugin add @clusterio/plugin-subspace_storage
|
||||
3. Follow the instructions given.
|
||||
|
||||
This adds it to the default `plugin-list.json` file which in the shared folder setup is loaded by controller, host and ctl.
|
||||
If you have hosts or ctl installed on separate computers (or directories) then you need to repeat the plugin install process for all of them.
|
||||
The clusteriocontroller, clusteriohost and clusterioctl commands has the plugin sub-command so you do not need to install clusteriocontroller to add plugins, instead use the clusterio command you have available.
|
||||
3.5 Some of the instructions are outdated. If you get stuck somewhere, look at the Ubuntu section.
|
||||
|
||||
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).
|
||||
4. Type `node client.js start [instancename]` to create a new instance.
|
||||
|
||||
To connect to a master server running on a remote machine, open config.json with your favourite text editor (notepad++). You can also set it up to use the official server browser.
|
||||
|
||||
## Configure Controller
|
||||
Change `masterIP `to something like `31.152.123.14` (provided by master server owner)
|
||||
|
||||
By default the controller will listen for HTTP on port 8080.
|
||||
You can change the port used with the command
|
||||
Change `masterPort` to something like `8080` (provided by master server owner)
|
||||
|
||||
npx clusteriocontroller config set controller.http_port 1234
|
||||
Change `masterAuthToken` to the value found in `secret-api-token.txt` on the master server
|
||||
|
||||
When changing the port you will also need to change the address hosts connect with.
|
||||
For the standalone installation mode you can use
|
||||
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)
|
||||
|
||||
npx clusteriohost config set host.controller_url http://localhost:1234/
|
||||
**GameClient**
|
||||
|
||||
If you plan to make your cluster available externally set the address
|
||||
that it will be accessible under with, for example
|
||||
Fancy game client that does the following steps automatically, but is really old so be warned: [clusterioClient](https://github.com/Danielv123/factorioClusterioClient)
|
||||
|
||||
npx clusteriocontroller config set controller.public_url http://203.0.113.4:1234/
|
||||
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
|
||||
|
||||
Change the url to reflect the IP, protocol, and port the controller is accessible under, dns names are also supported.
|
||||
If you're planning on making the controller accessible on the internet it's recommended to set up TLS, see the [Setting Up TLS](/docs/setting-up-tls.md) document for more details.
|
||||
2. Drop it into ./factorio/mods
|
||||
|
||||
You can list the config of the controller with the `npx clusteriocontroller config list` command.
|
||||
See the [readme for @clusterio/controller](/packages/controller/README.md) for more information.
|
||||
|
||||
|
||||
## Managing Factorio mods
|
||||
|
||||
Mods in clusterio are managed as parts of modpacks. There is currently no mod portal integration, so you first need to navigate to the mods section of the web UI and upload your mods. You can then create a modpack from the uploaded mods by navigating to the mods page, hitting `create`. Once you have a modpack, you can assign it to an instance under Instance config -> Factorio -> Mod pack. You can also set a default modpack for new instances in the controller settings.
|
||||
|
||||

|
||||
|
||||
Mod settings can be managed under the modpack configuration page in the web UI. For mod settings to show, you first need to run an export. This is done by creating an instance with the modpack assigned, opening the instance page and pressing More -> Export data.
|
||||
|
||||

|
||||
|
||||
Once this is done, the mod settings will show up under the modpack configuration page.
|
||||
|
||||

|
||||
|
||||
|
||||
## Setting up shared storage
|
||||
|
||||
The chests teleporting items to and from the shared storage on the controller has been moved into the Subspace Storage mod and plugin.
|
||||
To get this to work you will need to install the Subspace Storage plugin and copy the [Subspace Storage mod](https://mods.factorio.com/mod/subspace_storage) and the [Clusterio Lib mod](https://mods.factorio.com/mod/clusterio_lib) into the `sharedMods` folder on each host installation.
|
||||
|
||||
Do not install the [Clusterio mod](https://mods.factorio.com/mod/clusterio), this was for the old 1.2.x version of Clusterio and is not compatible with Clusterio 2.0.
|
||||
|
||||
|
||||
## Running Clusterio
|
||||
|
||||
After completing the installation start up the controller and at least one host separately.
|
||||
The installer provides the `run-controller` and `run-host` scripts to make this simple.
|
||||
Once the controller process is running you can log into the web interface which is hosted by default on http://localhost:8080/ (adjust the port number if you changed it), use the admin authentication token provided from the installation to log in.
|
||||
|
||||
The basics of setting up a Factorio server from the web interface is to create an instance, assign it to a host and then click start. You can also use the "Connect via Steam" button to launch Factorio directly and connect to the server using Steam's protocol handler.
|
||||
|
||||
### Running via Systemd
|
||||
|
||||
The install script creates systemd service scripts for clusteriocontroller and clusteriohost (if applicable) to start up as the user than ran the installer.
|
||||
If you have copied these files over to `/etc/systemd/system/` then you can startup Clusterio as a background service using:
|
||||
|
||||
#### To start as a service run:
|
||||
|
||||
sudo systemctl start clusteriocontroller # for the controller
|
||||
sudo systemctl start clusteriohost # for each physical server intended to host Factorio servers
|
||||
|
||||
#### To automatically get it to start on boot:
|
||||
|
||||
sudo systemctl enable clusteriocontroller # for the controller
|
||||
sudo systemctl enable clusteriohost # for each physical server intended to host Factorio servers
|
||||
|
||||
|
||||
## Setting up remote hosts
|
||||
|
||||
Run the installer as described in the installation section and choose "Host only" as the operating mode to install, it'll ask for a controller URL and an authentication token.
|
||||
The URL is the same as what is needed to connect to the web interface, and the authentication token can be generated on the Hosts page in the web interface.
|
||||
Once you start up the host it should show up in the Hosts list and be available for assigning and running instances on.
|
||||
|
||||
|
||||
## Setting up clusterioctl
|
||||
|
||||
There's a command line interface available for Clusterio which is installed separately with the same installer as for the controller and host.
|
||||
Run the installer as described in the installation section and choose "Ctl only" as the operating mode to install, you can do this in the same directory as you have installed other Clusterio component(s) to.
|
||||
The installer will ask for a controller URL and an authentication token, these are the same as you would use to connect to the web interface.
|
||||
If you want to use a different user for the command line interface, you can generate an authentication token for an existing user with
|
||||
|
||||
npx clusteriocontroller bootstrap generate-user-token <username>
|
||||
|
||||
|
||||
## Known Issues
|
||||
|
||||
- When the `server_select` plugin is installed you may see `[error] Unhandled event server_select:UpdateInstancesEvent` logged when starting an instance.
|
||||
This should be ignored as it does not indicate an error occured.
|
||||
- After updating Factorio you may get the error `Error: Expected empty response but got "Cannot execute command. Error: [string "clusterio_private.update_instance(..."]:1: attempt to index global 'clusterio_private' (a nil value)?` on instance start.
|
||||
This is due to Factorio applying a migration replacing the patched content of the save with the original scenario, attempting to start the instance again should resolve the issue.
|
||||
- In the mod pack view of the web interface the "base" mod is displayed with an error complaining about the mod being missing from the controller storage.
|
||||
This is due to the "base" mod currently not being connectly handled as a built-in mod to Factorio in the web interface, this error message should be ignored.
|
||||
- After migrating from Alpha 13 to Alpha 14 running an instance data export fails with the error `Upload failed: 401 Unauthorized` due to the host token being issued to a slave.
|
||||
To workaround this find the id of the host in the host config and generate a new host token with the same id.
|
||||
Stop the host and replace the value of the `host.controller_token` field in `config-host.json` with the new host token before starting it up again.
|
||||
- After migrating from Alpha 13 to Alpha 14 you get kicked out from the web interface due to the master to controller rename.
|
||||
If you don't have the authentication token readily available you can manually migrate the token by opening the Inspect/Developer Tools on the page and running the following piece of JavaScript in the Console:
|
||||
```
|
||||
localStorage.controller_token = localStorage.master_token
|
||||
```
|
||||
3. Run factorio and connect to slave as a normal MP game. You will find the port number to connect to at http://[masterAddress]:8080
|
||||
|
||||
## Plugins
|
||||
Here are the known Clusterio plugins in the wild:
|
||||
1. [Player Manager](https://github.com/Danielv123/playerManager) - Adds player management to the Web UI and shared inventory handling (beta)
|
||||
2. [DiscordChat](https://github.com/jakedraddy/ClusterioDiscordChat) - Logs in-game chat/joins/leave messages on all instances to a Discord webhook.
|
||||
3. [TrainTeleports](https://github.com/Godmave/clusterioTrainTeleports) - Allows you to teleport cargotrains between servers.
|
||||
|
||||
## Common problems
|
||||
|
||||
### Cannot find module: `/../../config`
|
||||
|
||||
Copy your config.json.dist to config.json and configure it.
|
||||
|
||||
### EACCESS [...] LISTEN 443
|
||||
|
||||
Some systems don't let non root processes listen to ports below 1000.
|
||||
Either run with `sudo` or change config.json to use higher port numbers.
|
||||
Some systems don't let non root processes listen to ports below 1000. Either run with `sudo` or change config.json to use higher port numbers.
|
||||
|
||||
According to [this link](https://askubuntu.com/questions/839520/open-port-443-for-a-node-web-app) if you manually installed node.js following the above instructions, you may need to run the following command to fix this issue:
|
||||
### InitializationError: First argument must be an abstract-leveldown compliant store
|
||||
|
||||
sudo setcap 'cap_net_bind_service=+ep' $(readlink -f $(which node))
|
||||
do:
|
||||
|
||||
### Port forwarding doesn't work on the controller when running under WSL
|
||||
npm install linvodb3@3.25.1
|
||||
|
||||
If you follow the ubuntu guide on WSL (Windows Subsystem for Linux, Bash on Ubuntu on Windows specifically), you will find that the website works on localhost and on your local ip, but not on the global ip.
|
||||
This is also true when you correctly port-forwarded the correct ports.
|
||||
Even when routing this server through nginx in WSL, the issue persists.
|
||||
Then, on a hunch, I tried to run nginx from windows itself and found that this DID work.
|
||||
It came to me that the only usage difference between the 2 versions of nginx is that I got a Windows Firewall popup.
|
||||
if that does not suffice do
|
||||
|
||||
npm install levelup@1.3.9
|
||||
|
||||
If it still doesn't work, come over to our support discord linked at the top of this readme.
|
||||
|
||||
### Portforwarding doesn't work on the master server when running under WSL
|
||||
|
||||
If you follow the ubuntu guide on WSL (Windows Subsystem for Linux, Bash on Ubuntu on Windows specifically), you will find that the website works on localhost and on your local ip, but not on the global ip. This is also true when you correctly port-forwarded the correct ports. Even when routing this server through nginx in WSL, the issue persists. Then, on a hunch, I tried to run nginx from windows itself and found that this DID work. It came to me that the only usage difference between the 2 versions of nginx is that I got a Windows Firewall popup.
|
||||
|
||||
TLDR: the tested fix is:
|
||||
|
||||
@@ -319,3 +253,36 @@ 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
|
||||
```
|
||||
|
||||
3. Download the latest version of the factorioClusterioMod from its github repo
|
||||
```
|
||||
node client.js manage shared mods add clusterio
|
||||
```
|
||||
@@ -1,119 +0,0 @@
|
||||
/**
|
||||
* Clean the repository of build and execution artifacts.
|
||||
*
|
||||
* clean: A quick clean that removes all build artifacts making the output safe to publish
|
||||
* clean-tests: Removes all build and test artifacts should not be required if tests are setup correctly.
|
||||
* clean-all: Removes all build, test, and execution artifacts. Including cluster config and logs!
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
const path = require("path");
|
||||
const fs = require("fs-extra");
|
||||
const yargs = require("yargs");
|
||||
|
||||
const npmPackage = require("./package.json");
|
||||
const workspaces = npmPackage.workspaces
|
||||
.map(p => p.slice(0, -2))
|
||||
.filter(p => !p.includes("external_plugins"));
|
||||
|
||||
let DRY = false;
|
||||
|
||||
/**
|
||||
* Attempt to remove a file / directory if it exists.
|
||||
*
|
||||
* @param {string} filePath The path to remove.
|
||||
*/
|
||||
async function tryRemove(filePath) {
|
||||
if (await fs.exists(filePath)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(filePath);
|
||||
}
|
||||
|
||||
if (!DRY) {
|
||||
await fs.remove(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all build artifacts
|
||||
*/
|
||||
async function removeBuildArtifacts() {
|
||||
const tasks = [];
|
||||
|
||||
for (const workspace of workspaces) {
|
||||
const basePath = path.resolve(workspace);
|
||||
const packages = await fs.readdir(basePath);
|
||||
for (const packageName of packages) {
|
||||
tasks.push(
|
||||
tryRemove(path.join(basePath, packageName, "dist"))
|
||||
);
|
||||
tasks.push(
|
||||
tryRemove(path.join(basePath, packageName, "node_modules", ".cache"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(tasks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all test artifacts
|
||||
*/
|
||||
async function removeTestArtifacts() {
|
||||
const artifacts = [
|
||||
".nyc_output", "coverage", "temp", "static",
|
||||
];
|
||||
|
||||
await Promise.all(artifacts.map(artifact => tryRemove(path.resolve(artifact))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all execution artifacts
|
||||
*/
|
||||
async function removeExecutionArtifacts() {
|
||||
const artifacts = [
|
||||
"database", "instances", "logs", "mods",
|
||||
"config-control.json", "config-controller.json", "config-host.json",
|
||||
"FactorioAdminToken.txt", "plugin-list.json",
|
||||
];
|
||||
|
||||
await Promise.all(artifacts.map(artifact => tryRemove(path.resolve(artifact))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function for this script
|
||||
*/
|
||||
async function main() {
|
||||
await yargs(process.argv.slice(2))
|
||||
.scriptName("clean")
|
||||
.option("dry-run", {
|
||||
alias: "dry",
|
||||
nargs: 0,
|
||||
describe: "Will not delete any files",
|
||||
default: false,
|
||||
type: "boolean",
|
||||
})
|
||||
.command("all", "Remove all artifacts", () => {}, argv => {
|
||||
DRY = argv.dry;
|
||||
removeBuildArtifacts();
|
||||
removeTestArtifacts();
|
||||
removeExecutionArtifacts();
|
||||
})
|
||||
.command("tests", "Remove all build and test artifacts", () => {}, argv => {
|
||||
DRY = argv.dry;
|
||||
removeBuildArtifacts();
|
||||
removeTestArtifacts();
|
||||
})
|
||||
.command("fast", "Remove all build artifacts", () => {}, argv => {
|
||||
DRY = argv.dry;
|
||||
removeBuildArtifacts();
|
||||
})
|
||||
.demandCommand(1, "A command must be specified")
|
||||
.strict()
|
||||
.parse();
|
||||
}
|
||||
|
||||
// Run main if started from command line
|
||||
if (module === require.main) {
|
||||
main();
|
||||
}
|
||||
@@ -0,0 +1,797 @@
|
||||
const fs = require('fs-extra');
|
||||
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').Rcon;
|
||||
const hashFiles = require('hash-files');
|
||||
const _ = require('underscore');
|
||||
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("_app/fileOps");
|
||||
const stringUtils = require("./lib/stringUtils.js");
|
||||
const configManager = require("./lib/manager/configManager.js");
|
||||
|
||||
// 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 = {
|
||||
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),
|
||||
}
|
||||
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();
|
||||
client.connect({
|
||||
host: 'localhost',
|
||||
port: Number(process.env.RCONPORT) || instconf.clientPort,
|
||||
password: instconf.clientPassword,
|
||||
timeout: 5000
|
||||
});
|
||||
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();
|
||||
|
||||
// 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,
|
||||
timeout: 5000
|
||||
});
|
||||
} 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
|
||||
_.once(instanceManagement);
|
||||
async function instanceManagement(instanceconfig) {
|
||||
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 log = function(message) {
|
||||
console.log("Clusterio | "+ pluginsToLoad[i].name + " | " + message);
|
||||
}
|
||||
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] != "/") {
|
||||
log(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);
|
||||
|
||||
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,
|
||||
}
|
||||
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");
|
||||
}
|
||||
function callback2(hash, modName){
|
||||
hashedMods[hashedMods.length] = {
|
||||
modName: modName,
|
||||
hash: hash,
|
||||
}
|
||||
// check if this callback has ran once for each mod
|
||||
if(hashedMods.length == instanceMods.length) {
|
||||
callback(hashedMods);
|
||||
}
|
||||
//console.log(modname);
|
||||
}
|
||||
let hashedMods = [];
|
||||
/*let mods = fs.readdirSync("./sharedMods/")*/
|
||||
let instanceMods = fs.readdirSync(config.instanceDirectory+"/"+instanceName+"/mods/");
|
||||
if(instanceMods.length == 0){
|
||||
callback({}); // there are no mods installed, return an empty object
|
||||
}
|
||||
for(let o=0;o<instanceMods.length;o++) {
|
||||
if(path.extname(instanceMods[o]) != ".zip") {
|
||||
instanceMods.splice(instanceMods.indexOf(instanceMods[o]), 1); // remove element from array, we can't hash unzipped mods
|
||||
}
|
||||
}
|
||||
for(let i=0; i<instanceMods.length; i++){
|
||||
let path = config.instanceDirectory+"/"+instanceName+"/mods/"+instanceMods[i];
|
||||
let name = instanceMods[i];
|
||||
let options = {
|
||||
files:path,
|
||||
}
|
||||
// options {files:[array of paths]}
|
||||
hashFiles(options, function(error, hash) {
|
||||
// hash will be a string if no error occurred
|
||||
if(!error){
|
||||
callback2(hash, name);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
const needle = require("needle");
|
||||
const config = require("./config.json");
|
||||
|
||||
const masterIP = `${config.masterIP}:${config.masterPort}`;
|
||||
const token = config.masterAuthToken
|
||||
let commandArray = process.argv;
|
||||
commandArray.shift();
|
||||
commandArray.shift();
|
||||
const needleOptionsWithTokenAuthHeader = {
|
||||
headers: {
|
||||
'x-access-token': token,
|
||||
},
|
||||
};
|
||||
|
||||
let command = commandArray.join(" ");
|
||||
let killBiters = `/silent-command local surface=game.surfaces[1] for key, entity in pairs(surface.find_entities_filtered({force="enemy"})) do entity.destroy() end`;
|
||||
|
||||
(async ()=>{
|
||||
needle.post(masterIP+"/api/runCommand", {
|
||||
broadcast:true,
|
||||
command,
|
||||
}, needleOptionsWithTokenAuthHeader, (err, resp) => {
|
||||
console.log(resp.body)
|
||||
});
|
||||
// setInterval(()=>{
|
||||
// needle.post(masterIP+"/api/runCommand", {
|
||||
// broadcast:true,
|
||||
// command: killBiters,
|
||||
// }, needleOptionsWithTokenAuthHeader, (err, resp) => {
|
||||
// console.log(resp.body)
|
||||
// });
|
||||
// }, 1000*60*5);
|
||||
})();
|
||||
@@ -1,6 +0,0 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
if_ci_failed: success
|
||||
informational: true
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"__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,
|
||||
"disableFairItemDistribution": false,
|
||||
"uploadModsToMaster": true,
|
||||
"msBetweenCommands": 10,
|
||||
"allowRemoteCommandExecution": true,
|
||||
"enableCrossServerShout": true,
|
||||
"mirrorAllChat": false,
|
||||
"__comment7": "Autosave interval for items in the master storage in milliseconds. Defaults to one minute when set to 0 or not set.",
|
||||
"autosaveInterval": 600000,
|
||||
"disablePrometheusPushgateway": false,
|
||||
"__comment8": "This setting only affects the master. It makes every import for things that arent 'electricity' return a maximum of 0. Effectively disables teleport chests.",
|
||||
"disableImportsOfEverythingExceptElectricity": false
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
unit_test:
|
||||
image: clusterio
|
||||
build: .
|
||||
command: npm run test
|
||||
@@ -1,9 +0,0 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
controller:
|
||||
build: .
|
||||
command: npx clusteriocontroller run
|
||||
host:
|
||||
build: .
|
||||
command: npx clusteriohost run
|
||||
@@ -1,3 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1000px" height="840px" viewBox="-0.5 -0.5 1000 840" style="background-color: rgb(238, 243, 250);"><defs/><g><rect x="40" y="550" width="320" height="250" fill="none" stroke="#000000" stroke-dasharray="3 3" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-end; justify-content: unsafe flex-start; width: 318px; height: 1px; padding-top: 547px; margin-left: 42px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">connected</div></div></div></foreignObject><text x="42" y="547" fill="#000000" font-family="Helvetica" font-size="12px">connected</text></switch></g><rect x="780" y="40" width="180" height="150" fill="#cedbee" stroke="none" pointer-events="all"/><path d="M 300 750 L 300 730 L 440 730 L 440 140 L 376.37 140" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 371.12 140 L 378.12 136.5 L 376.37 140 L 378.12 143.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 723px; margin-left: 346px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">yes</div></div></div></foreignObject><text x="346" y="726" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">yes</text></switch></g><rect x="520" y="490" width="440" height="310" fill="none" stroke="#000000" stroke-dasharray="3 3" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-end; justify-content: unsafe flex-start; width: 438px; height: 1px; padding-top: 487px; margin-left: 522px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">resuming</div></div></div></foreignObject><text x="522" y="487" fill="#000000" font-family="Helvetica" font-size="12px">resuming</text></switch></g><rect x="40" y="210" width="380" height="300" fill="none" stroke="#000000" stroke-dasharray="3 3" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-end; justify-content: unsafe flex-start; width: 378px; height: 1px; padding-top: 207px; margin-left: 42px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">connecting</div></div></div></foreignObject><text x="42" y="207" fill="#000000" font-family="Helvetica" font-size="12px">connecting</text></switch></g><path d="M 120 150 L 120 223.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 120 228.88 L 116.5 221.88 L 120 223.63 L 123.5 221.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 191px; margin-left: 151px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 11px; font-family: Helvetica; coLine truncated
|
||||
|
Before Width: | Height: | Size: 78 KiB |
@@ -1,3 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="919px" height="620px" viewBox="-0.5 -0.5 919 620" style="background-color: rgb(238, 243, 250);"><defs/><g><rect x="40" y="260" width="320" height="300" fill="none" stroke="#000000" stroke-dasharray="3 3" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-end; justify-content: unsafe flex-start; width: 318px; height: 1px; padding-top: 257px; margin-left: 42px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">connected</div></div></div></foreignObject><text x="42" y="257" fill="#000000" font-family="Helvetica" font-size="12px">connected</text></switch></g><rect x="700" y="40" width="180" height="150" fill="#cedbee" stroke="none" pointer-events="all"/><path d="M 280 390 L 280 370 L 380 370 L 380 130 L 346.37 130" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 341.12 130 L 348.12 126.5 L 346.37 130 L 348.12 133.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 360px; margin-left: 291px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">yes</div></div></div></foreignObject><text x="291" y="363" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">yes</text></switch></g><rect x="420" y="190" width="160" height="160" fill="none" stroke="#000000" stroke-dasharray="3 3" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-end; justify-content: unsafe flex-start; width: 158px; height: 1px; padding-top: 187px; margin-left: 422px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">resuming</div></div></div></foreignObject><text x="422" y="187" fill="#000000" font-family="Helvetica" font-size="12px">resuming</text></switch></g><path d="M 120 140 L 120 273.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 120 278.88 L 116.5 271.88 L 120 273.63 L 123.5 271.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 194px; margin-left: 151px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">ready()</div></div></div></foreignObject><text x="151" y="197" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">ready()</text></switch></g><rect x="40" y="80" width="320" height="80" fill="none" stroke="#000000" stroke-dasharray="3 3" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-end; justify-content: unsafe flex-start; width: 318px; height: 1px; padding-top: 77px; margin-left: 42px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #00Line truncated
|
||||
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 54 KiB |
@@ -1,321 +0,0 @@
|
||||
<mxfile host="Electron" modified="2021-08-30T18:05:06.237Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/14.9.6 Chrome/89.0.4389.128 Electron/12.0.16 Safari/537.36" etag="uzcLt7Oh4qtDsMY9WSdT" compressed="false" version="14.9.6" type="device">
|
||||
<diagram id="GSbHU_PhJe-l3NQpc0X3" name="Connection Flow">
|
||||
<mxGraphModel dx="710" dy="734" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="760" pageHeight="630" background="#EEF3FA" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="GTJXd52-XhRZD3rAr2nm-0" />
|
||||
<mxCell id="GTJXd52-XhRZD3rAr2nm-1" parent="GTJXd52-XhRZD3rAr2nm-0" />
|
||||
<mxCell id="2r3fOvGVNr9_K5ZBoRis-18" value="Client" style="text;html=1;align=left;verticalAlign=middle;resizable=1;points=[];autosize=0;strokeColor=none;fontStyle=1" parent="GTJXd52-XhRZD3rAr2nm-1" vertex="1">
|
||||
<mxGeometry x="440" y="80" width="145" height="20" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="2r3fOvGVNr9_K5ZBoRis-28" target="IKjVovVhkWe_WipwIJL3-0">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="260" y="320.02" />
|
||||
<mxPoint x="260" y="320.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-5" value="receive<br>resume" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;spacingRight=4;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-1">
|
||||
<mxGeometry x="0.086" relative="1" as="geometry">
|
||||
<mxPoint x="-20" y="-6" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="2r3fOvGVNr9_K5ZBoRis-28" target="IKjVovVhkWe_WipwIJL3-2">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="180" y="320.02" />
|
||||
<mxPoint x="180" y="320.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-26" value="receive<br>register" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;spacingRight=4;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-3">
|
||||
<mxGeometry x="-0.2268" relative="1" as="geometry">
|
||||
<mxPoint x="-20" y="2" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-14" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="2r3fOvGVNr9_K5ZBoRis-28" target="IKjVovVhkWe_WipwIJL3-13">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="40" y="285.02" />
|
||||
<mxPoint x="40" y="580.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-27" value="bad handshake<br>timeout" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-14">
|
||||
<mxGeometry x="-0.6341" y="-2" relative="1" as="geometry">
|
||||
<mxPoint x="28" y="17" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="2r3fOvGVNr9_K5ZBoRis-28" value="Wait for handshake" style="rounded=0;whiteSpace=wrap;html=1;strokeWidth=2;" parent="GTJXd52-XhRZD3rAr2nm-1" vertex="1">
|
||||
<mxGeometry x="160" y="270.02000000000004" width="120" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="HutjLCx0ygpoBEJLSh69-0" value="Server" style="text;html=1;align=left;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fontStyle=1" parent="GTJXd52-XhRZD3rAr2nm-1" vertex="1">
|
||||
<mxGeometry x="40" y="80" width="50" height="20" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="_PUvpjEMIo1z1dIbIUzw-0" value="<p><span>Connection Flow</span><br></p>" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontStyle=0;fontSize=20;" parent="GTJXd52-XhRZD3rAr2nm-1" vertex="1">
|
||||
<mxGeometry x="40" y="30" width="230" height="20" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="fGsjKHM69yN9R0Hf5M05-1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="fGsjKHM69yN9R0Hf5M05-0" target="IKjVovVhkWe_WipwIJL3-7">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="220" y="180" />
|
||||
<mxPoint x="180" y="180" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="fGsjKHM69yN9R0Hf5M05-0" value="WebSocket Open" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="160" y="119.98" width="120" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-0" target="IKjVovVhkWe_WipwIJL3-9">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="300" y="300.02" />
|
||||
<mxPoint x="300" y="300.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-6" value="bad session" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;spacingLeft=4;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-4">
|
||||
<mxGeometry x="-0.429" y="-1" relative="1" as="geometry">
|
||||
<mxPoint x="29" y="-4" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-20" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-0" target="IKjVovVhkWe_WipwIJL3-19">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-29" value="valid" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-20">
|
||||
<mxGeometry x="-0.1944" y="2" relative="1" as="geometry">
|
||||
<mxPoint x="18" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-0" value="Validate resume" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="230" y="340.02" width="120" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-21" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-2" target="IKjVovVhkWe_WipwIJL3-18">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-28" value="valid" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-21">
|
||||
<mxGeometry x="-0.2778" relative="1" as="geometry">
|
||||
<mxPoint x="20" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-24" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-2" target="IKjVovVhkWe_WipwIJL3-13">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="60" y="355.02" />
|
||||
<mxPoint x="60" y="560.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-30" value="bad token" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-24">
|
||||
<mxGeometry x="-0.8093" y="1" relative="1" as="geometry">
|
||||
<mxPoint x="29" y="23" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-2" value="Validate register" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="90" y="340.02" width="120" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-8" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-7" target="2r3fOvGVNr9_K5ZBoRis-28">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="180" y="260.02" />
|
||||
<mxPoint x="180" y="260.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-7" value="Send hello" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="90" y="199.96" width="120" height="30.02" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-10" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-9" target="2r3fOvGVNr9_K5ZBoRis-28">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="260" y="260.02" />
|
||||
<mxPoint x="260" y="260.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-9" value="Send invalidate" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="230" y="199.96" width="120" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-13" value="Close connection" style="rounded=1;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="160" y="550.02" width="120" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-15" value="Connected" style="rounded=1;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="160" y="480.02" width="120" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-22" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-18" target="IKjVovVhkWe_WipwIJL3-15">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="180" y="470" />
|
||||
<mxPoint x="180" y="470" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-18" value="Send ready" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="90" y="410.02" width="120" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-23" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-19" target="IKjVovVhkWe_WipwIJL3-15">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="260" y="470" />
|
||||
<mxPoint x="260" y="470" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-19" value="Send continue" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="230" y="410.02" width="120" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-31" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-37" target="IKjVovVhkWe_WipwIJL3-44">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="630" y="240.02" />
|
||||
<mxPoint x="630" y="240.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-32" value="previous<br>session" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;spacingRight=8;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-31">
|
||||
<mxGeometry x="0.086" relative="1" as="geometry">
|
||||
<mxPoint x="-20" y="-2" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-33" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-37" target="IKjVovVhkWe_WipwIJL3-49">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="550" y="250.02" />
|
||||
<mxPoint x="550" y="250.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-34" value="no<br>session" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;spacingRight=8;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-33">
|
||||
<mxGeometry x="-0.2268" relative="1" as="geometry">
|
||||
<mxPoint x="-20" y="5" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-35" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-37" target="IKjVovVhkWe_WipwIJL3-54">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="440" y="215.02" />
|
||||
<mxPoint x="440" y="570.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-36" value="bad handshake<br>timeout" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-35">
|
||||
<mxGeometry x="-0.6341" y="-2" relative="1" as="geometry">
|
||||
<mxPoint x="35" y="-23" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-37" value="Wait for hello" style="rounded=0;whiteSpace=wrap;html=1;strokeWidth=2;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="530" y="199.96000000000006" width="120" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-60" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-39" target="IKjVovVhkWe_WipwIJL3-37">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-39" value="WebSocket Open" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="530" y="119.98" width="120" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-65" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-44" target="IKjVovVhkWe_WipwIJL3-61">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="660" y="360.02" />
|
||||
<mxPoint x="660" y="360.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-44" value="Send resume" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="600" y="270.0199999999999" width="120" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-68" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-49" target="IKjVovVhkWe_WipwIJL3-67">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-49" value="Send register" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="460" y="270.0199999999999" width="120" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-54" value="Close connection" style="rounded=1;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="530" y="550.02" width="120" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-55" value="Connected" style="rounded=1;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="530" y="480.0199999999999" width="120" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-64" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-61" target="IKjVovVhkWe_WipwIJL3-62">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="630" y="390.02" />
|
||||
<mxPoint x="630" y="390.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-73" value="receive<br>invalidate" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-64">
|
||||
<mxGeometry x="0.2133" relative="1" as="geometry">
|
||||
<mxPoint x="-30" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-70" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-61" target="IKjVovVhkWe_WipwIJL3-54">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="700" y="570.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-74" value="bad handshake<br>timeout" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;rotation=90;spacingTop=7;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-70">
|
||||
<mxGeometry x="-0.7644" y="2" relative="1" as="geometry">
|
||||
<mxPoint x="-12" y="29" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-71" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-61" target="IKjVovVhkWe_WipwIJL3-55">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="630" y="450.02" />
|
||||
<mxPoint x="630" y="450.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-75" value="receive<br>continue" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];spacingRight=4;labelBackgroundColor=none;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-71">
|
||||
<mxGeometry x="-0.3167" y="-1" relative="1" as="geometry">
|
||||
<mxPoint x="-19" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-61" value="Wait for continue" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;strokeWidth=2;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="600" y="410.02" width="120" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-63" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-62" target="IKjVovVhkWe_WipwIJL3-49">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="550" y="320.02" />
|
||||
<mxPoint x="550" y="320.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-62" value="Invalidate session" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="530" y="340.02" width="120" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-69" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-67" target="IKjVovVhkWe_WipwIJL3-54">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="470" y="560.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-77" value="bad handshake<br>timeout" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];rotation=90;labelBackgroundColor=none;spacingBottom=4;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-69">
|
||||
<mxGeometry x="-0.8148" y="1" relative="1" as="geometry">
|
||||
<mxPoint x="12" y="33" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-72" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="GTJXd52-XhRZD3rAr2nm-1" source="IKjVovVhkWe_WipwIJL3-67" target="IKjVovVhkWe_WipwIJL3-55">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="550" y="470.02" />
|
||||
<mxPoint x="550" y="470.02" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-76" value="receive<br>ready" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;" vertex="1" connectable="0" parent="IKjVovVhkWe_WipwIJL3-72">
|
||||
<mxGeometry x="-0.2389" y="2" relative="1" as="geometry">
|
||||
<mxPoint x="-22" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="IKjVovVhkWe_WipwIJL3-67" value="Wait for ready" style="rounded=0;whiteSpace=wrap;html=1;labelBackgroundColor=none;strokeWidth=2;" vertex="1" parent="GTJXd52-XhRZD3rAr2nm-1">
|
||||
<mxGeometry x="460" y="410.02" width="120" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -1,3 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="760px" height="630px" viewBox="-0.5 -0.5 760 630" style="background-color: rgb(238, 243, 250);"><defs/><g><rect x="440" y="80" width="145" height="20" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 1px; height: 1px; padding-top: 90px; margin-left: 442px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; font-weight: bold; white-space: nowrap; ">Client</div></div></div></foreignObject><text x="442" y="94" fill="#000000" font-family="Helvetica" font-size="12px" font-weight="bold">Client</text></switch></g><path d="M 260 300.02 L 260 320 L 260 333.65" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 260 338.9 L 256.5 331.9 L 260 333.65 L 263.5 331.9 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 316px; margin-left: 238px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">receive<br />resume</div></div></div></foreignObject><text x="238" y="319" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">receive...</text></switch></g><path d="M 180 300.02 L 180 320 L 180 333.65" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 180 338.9 L 176.5 331.9 L 180 333.65 L 183.5 331.9 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 318px; margin-left: 158px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">receive<br />register</div></div></div></foreignObject><text x="158" y="322" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">receive...</text></switch></g><path d="M 160 285 L 40 285 L 40 580 L 153.63 580" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 158.88 580 L 151.88 583.5 L 153.63 580 L 151.88 576.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 300px; margin-left: 91px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">bad handshake<br />timeout</div></div></div></foreignObject><text x="91" y="304" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">bad handshake...</text></switch></g><rect x="160" y="270.02" width="120" height="30" fill="#ffffff" stroke="#000000" stroke-width="2" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 285px; margin-left: 161px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "Line truncated
|
||||
|
Before Width: | Height: | Size: 39 KiB |
@@ -1,3 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="800px" height="440px" viewBox="-0.5 -0.5 800 440" style="background-color: rgb(238, 243, 250);"><defs/><g><path d="M 170 365.02 L 170 385 L 290 385 L 290 371.39" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 290 366.14 L 293.5 373.14 L 290 371.39 L 286.5 373.14 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><path d="M 110 350 L 40 350 L 40 130 L 103.63 130" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 108.88 130 L 101.88 133.5 L 103.63 130 L 101.88 126.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="110" y="335.02" width="100" height="30" fill="#ffffff" stroke="#000000" stroke-width="2" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 98px; height: 1px; padding-top: 350px; margin-left: 111px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">connected</div></div></div></foreignObject><text x="160" y="354" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">connected</text></switch></g><path d="M 290 335.02 L 290 315 L 180 315 L 180 328.65" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 180 333.9 L 176.5 326.9 L 180 328.65 L 183.5 326.9 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><path d="M 350 335.02 L 350 135 L 216.37 135" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 211.12 135 L 218.12 131.5 L 216.37 135 L 218.12 138.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><path d="M 320 335.02 L 320 235 L 216.37 235" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 211.12 235 L 218.12 231.5 L 216.37 235 L 218.12 238.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="270" y="335.02" width="100" height="30" fill="#ffffff" stroke="#000000" stroke-width="2" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 98px; height: 1px; padding-top: 350px; margin-left: 271px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">resuming</div></div></div></foreignObject><text x="320" y="354" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">resuming</text></switch></g><path d="M 500 149.96 L 500 223.59" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 500 228.84 L 496.5 221.84 L 500 223.59 L 503.5 221.84 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="460" y="119.96" width="80" height="30" fill="#ffffff" stroke="#000000" stroke-width="2" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 78px; height: 1px; padding-top: 135px; margin-left: 461px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">closed</div></div></div></foreignObject><text x="500" y="139" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">closed</text></switch></g><path d="M 520 259.96 L 520 280 L 630 280 L 630 266.33" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 630 261.08 L 633.5 268.08 L 630 266.33 L 626.5 268.08 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><path d="M 480 229.96 L 480 190 L 480 156.33" fill="noneLine truncated
|
||||
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 128 KiB |
@@ -1,36 +0,0 @@
|
||||
# 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 controller, host, instance, and control.
|
||||
Although host 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/src/link/messages.ts](/packages/lib/src/link/messages.ts) 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.
|
||||
@@ -1,76 +0,0 @@
|
||||
# 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 field based configuration system to manage settings for the controller, instances, hosts, and the clusterioctl utility.
|
||||
Plugins can add their own config fields for the controller and instances configs.
|
||||
The fields are defined early in the startup, and fields for disabled plugins will still be created.
|
||||
|
||||
The built-in config fields are defined in [packages/lib/src/config/definitions.ts](/packages/lib/src/config/definitions.ts) while plugins can add their own config field definitions to the `controllerConfigFields` and `instanceConfigFields` properties of the `plugin` export.
|
||||
A `<plugin_name>.load_plugin` field is automatically created for each plugin and it's used to enable/disable the loading plugins on startup.
|
||||
|
||||
|
||||
## Defing Fields
|
||||
|
||||
Fields are defined as POJS object properties where the name of the property correspond to the name of the configuration field.
|
||||
The property name 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 consist of a group name and a field name separated by a dot.
|
||||
For plugins the group name is the name of the plugin, e.g., the `level` field of the `foo_frobber` plugin would be named `foo_frobber.lavel`.
|
||||
|
||||
**access**:
|
||||
The locations this config field can be read and modified from.
|
||||
This is an array of strings which can contain the values `"controller"`, `"host"` and `"control"`.
|
||||
If a party is missing in this array then that party will not receive the value of this field when the config is shared, and will be unable to modify the field.
|
||||
If not set any party can read and modify this config entry.
|
||||
Note that instance config fields should have both `"controller"` and `"host"` or unexpected behaviour may occur.
|
||||
|
||||
**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.
|
||||
|
||||
**title** (optional):
|
||||
Text used to identify the config field in user interfaces.
|
||||
This should not contain the grouping used in `name`.
|
||||
|
||||
**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.
|
||||
|
||||
**initialValue** (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.
|
||||
E.g.:
|
||||
|
||||
let value = configInstance.get("group.field");
|
||||
|
||||
Or by through the raw fields object (which is best avoided as it doesn't check types or whether the accessed field exists).
|
||||
E.g.:
|
||||
|
||||
let value = configInstance.fields["group.field"]
|
||||
|
||||
The set method also 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 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 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 controller and serialized and sent over to the relevant hosts on startup.
|
||||
This makes it possible to edit instance configs when the host the instance is on is offline.
|
||||
A copy is also stored in the instance directory on the host, and this config will be deliverd to the controller on host startup, which enables instances to be copied between clusters as long as their IDs are unique.
|
||||
@@ -1,429 +0,0 @@
|
||||
# Configuration
|
||||
|
||||
|
||||
## Controller Configuration
|
||||
|
||||
### controller.name
|
||||
|
||||
Name of the cluster.
|
||||
Used to distinguish it from other clusters and is prepended to the name of servers in the server list.
|
||||
|
||||
Defaluts to "Your Cluster".
|
||||
|
||||
|
||||
### controller.database_directory
|
||||
|
||||
Directory used to store cluster data to.
|
||||
Needs to be writeable by the controller.
|
||||
This is also used by plugins to store its data.
|
||||
|
||||
Defaults to "database".
|
||||
|
||||
|
||||
### controller.http_port
|
||||
|
||||
Port to host HTTP server on.
|
||||
If set to null no HTTP server will be exposed.
|
||||
At least one of this and controller.https_port needs to be set to a port.
|
||||
|
||||
Defaults to 8080.
|
||||
|
||||
|
||||
### controller.https_port
|
||||
|
||||
Port to host HTTPS server on.
|
||||
Uses controller.tls_certificate and controller.tls_private_key as the certificate and private key for the HTTPS server.
|
||||
If set to null no HTTPS server will be exposed.
|
||||
At least one of this and controller.http_port needs to be set to a port.
|
||||
|
||||
See the [Setting up TLS](/docs/setting-up-tls.md) document for a guide to setting up HTTPS with Clusterio.
|
||||
|
||||
Defaults to null.
|
||||
|
||||
|
||||
### controller.bind_address
|
||||
|
||||
IP address to bind the HTTP and HTTPS ports on.
|
||||
Useful to limit which interface to accept connections from.
|
||||
If set to null the unspecified IPv6 address will be used, which on most systems means it will also listen on the unspecified IPv4 address and accept connections from all
|
||||
interfaces.
|
||||
|
||||
Defaults to null.
|
||||
|
||||
|
||||
### controller.public_url
|
||||
|
||||
Public address the controller is accessible on.
|
||||
Currently only used for `clusteriocontroller bootstrap create-ctl-config` in order to give the right url to connect to.
|
||||
This should be a full URL ending with a /.
|
||||
|
||||
Defaults to null meaning assume localhost.
|
||||
|
||||
|
||||
### controller.tls_certificate
|
||||
|
||||
Path to TLS certificate to use for the HTTPS server when controller.https_port is configured.
|
||||
|
||||
Defaults to "database/certificates/cert.crt".
|
||||
|
||||
|
||||
### controller.tls_private_key
|
||||
|
||||
Path to TLS private key to use for the HTTPS server when controller.https_port is configured.
|
||||
|
||||
Defaults to "database/certificates/cert.key".
|
||||
|
||||
|
||||
### controller.auth_secret
|
||||
|
||||
Base64 encoded authentication secret used to verify tokens issued to hosts, users, and sessions.
|
||||
This should contain 256 bytes of random data, and if changed will cause all tokens become invalid.
|
||||
Keep this secret if leaked an attacker could easily compromise the cluster.
|
||||
|
||||
Defaults to null which means generate a secure secret on startup.
|
||||
|
||||
|
||||
### controller.heartbeat_interval
|
||||
|
||||
Interval in seconds heartbeats are sent out at for WebSocket connections.
|
||||
If a WebSocket connection hasn't received a heartbeat in 2 times the heartbeat interval it will be considered stale and closed.
|
||||
A lower value means less time between connections going stale and them being closed.
|
||||
|
||||
Defaults to 15.
|
||||
|
||||
|
||||
### controller.session_timeout
|
||||
|
||||
Time in seconds from the connection is closed following a heartbeat timeout until the session is invalidated and data loss occurs.
|
||||
A higher timeout gives more time for a client to recconect and resume an active session over a connection that was dropped, but also leads to stale connections taking longer to clear out.
|
||||
In the case of the controller or client crashing the data loss is unavoidable.
|
||||
|
||||
Value should not be less than the configured `max_reconnect_delay` of the clients and it should be greater than 2 times `controller.heartbeat_interval`.
|
||||
Defaults to 60.
|
||||
|
||||
|
||||
### controller.metrics_timeout
|
||||
|
||||
Timeout in seconds before a call to gather metrics from a host times out.
|
||||
This should be less than both the timeout and collection interval configured for the collection job in Prometheus.
|
||||
|
||||
Defaults to 8.
|
||||
|
||||
|
||||
### controller.stream_timeout
|
||||
|
||||
Timeout in seconds for proxy streams through the controller that has been created but is not yet sending data to be automatically closed.
|
||||
Streams that have started sending data before this timeout is not affected.
|
||||
|
||||
Defaults to 15.
|
||||
|
||||
|
||||
### controller.default_role_id
|
||||
|
||||
Role to automatically grant to new users.
|
||||
If null no role is granted.
|
||||
|
||||
Defaults to 1 which correspond to the default Player role.
|
||||
|
||||
|
||||
### <plugin_name>.load_plugin
|
||||
|
||||
Whether to load the plugin on the controller.
|
||||
Plugins not loaded on the controller will not be loaded on instances.
|
||||
|
||||
Defaults to true.
|
||||
|
||||
|
||||
## Host Configuration
|
||||
|
||||
### host.name
|
||||
|
||||
Name of the host.
|
||||
Shows up in host lists and is used to reference this host in the clusterioctl command line interface.
|
||||
|
||||
Defaults to "New Host".
|
||||
|
||||
|
||||
### host.id
|
||||
|
||||
Immutable numeric id of the host which uniquely identifies this host in the cluster.
|
||||
|
||||
Defaults to a random 31 bit number.
|
||||
|
||||
|
||||
### host.factorio_directory
|
||||
|
||||
Directory to look for the Factorio server(s) in.
|
||||
This can either point to a Factorio server directory, or to a directory containing multiple versions of the Factorio server.
|
||||
|
||||
Defaults to "factorio".
|
||||
|
||||
|
||||
### host.instances_directory
|
||||
|
||||
Directory to store instances in.
|
||||
One sub-directory will be created in it for each instance assigned to this host and these sub-directories will contain logs, saves, and various auto generated configuration files for the instances.
|
||||
|
||||
Defaults to "instances".
|
||||
|
||||
|
||||
### host.controller_url
|
||||
|
||||
URL to connect to the controller to.
|
||||
|
||||
Defaults to "http://localhost:8080/".
|
||||
|
||||
|
||||
### host.controller_token
|
||||
|
||||
Access token used for authenticating with the controller.
|
||||
You can generate an access token with `clusterioctl host generate-token --id <host-id>`, or use the `clusterioctl host create-config` to create a new host config with the correct url and token in it.
|
||||
|
||||
Defaults to "enter token here".
|
||||
|
||||
|
||||
### host.tls_ca
|
||||
|
||||
Path to certificate in PEM format to use for validating a TLS connection to the controller.
|
||||
If you have a self signed certificate on the controller you will need to copy the certificate to your hosts and set it as the certificate authority with this option.
|
||||
|
||||
Defaults to null meaning use Node.js's default set of trusted certificate authorities.
|
||||
|
||||
|
||||
### host.public_address
|
||||
|
||||
External address instances hosted on this host can be accessed on.
|
||||
This is used by plugins like Server Select to give the correct address to switch between instances in-game.
|
||||
|
||||
Defaults to "localhost".
|
||||
|
||||
|
||||
### host.max_reconnect_delay
|
||||
|
||||
Maximum delay in seconds to wait after connection to the controller is dropped before attempting to reconnect to it.
|
||||
The actual delay on each reconnect will be a random number between 0 and this configured value to avoid all clients trying to reconnect at the same time.
|
||||
|
||||
Defaults to 60.
|
||||
|
||||
|
||||
## Instance Configuration
|
||||
|
||||
### instance.name
|
||||
|
||||
Name of the instance, shown in instance lists and used to reference to this instance in the clusterioctl command line interface.
|
||||
|
||||
Defaults to "New Instance".
|
||||
|
||||
|
||||
### instance.id
|
||||
|
||||
Imutable numeric id of the instance which uniquely identifies this instance in the cluster.
|
||||
|
||||
Defaluts to a random 31 bit integer.
|
||||
|
||||
|
||||
### instance.assigned_host
|
||||
|
||||
Host this instance is assigned to.
|
||||
To change this you need to use the separate assign instance interface.
|
||||
After being assigned to a host the directory for the instance is created on the host.
|
||||
|
||||
Defaults to null meaning not assigned.
|
||||
|
||||
|
||||
### instance.auto_start
|
||||
|
||||
If enabled start this instance when the host it is assigned to is started up.
|
||||
Does not affect start or stop of the instance while the host is running.
|
||||
|
||||
Defaults to false.
|
||||
|
||||
|
||||
### instance.exclude_from_start_all
|
||||
|
||||
If enabled this instance will be excluded from the "Start all" button operation in the web interface.
|
||||
This allows you to prevent certain instances from being started when using the bulk start operation.
|
||||
|
||||
Defaults to false.
|
||||
|
||||
|
||||
### factorio.version
|
||||
|
||||
Version of Factorio to use for this instance.
|
||||
Can be a version like "1.0.0" or special string "latest" meaning the latest version of Factorio found on the host.
|
||||
|
||||
Defaults to "latest".
|
||||
|
||||
|
||||
### factorio.game_port
|
||||
|
||||
UDP port to run the Factorio server on.
|
||||
If null a random port in the dynamyc range assigned each time the instance starts.
|
||||
|
||||
Defaults to null.
|
||||
|
||||
|
||||
### factorio.rcon_port
|
||||
|
||||
TCP port to start the RCON interface on the Factorio server on.
|
||||
If null a random port in the dynamic range is assigned each time the instance starts.
|
||||
|
||||
Defaults to null.
|
||||
|
||||
|
||||
### factorio.rcon_password
|
||||
|
||||
Password to use for the RCON interafec.
|
||||
If null a random secure password is generated and used each time the instance starts.
|
||||
|
||||
Defaults to null.
|
||||
|
||||
|
||||
### player_online_autosave_slots
|
||||
|
||||
Keeps a separate pool for autosaves where at least on player has been online since the previous autosave was done.
|
||||
This requires regular autosaving to be enabled and will rename autosaves to `_autosave_poN.zip` (where N is a slot number) whenever a player has been online for that save.
|
||||
The slot numbers start at 1, are incremented by 1 after each autosave that is renamed and reset back to 1 whenever this value is exceeded.
|
||||
Note that this value should be set to a higher value than the value of `autosave_slots` in `factorio.settings`.
|
||||
Useful for preventing the loss of all progress when nobody is around on always online servers with auto pause disabled.
|
||||
|
||||
If 0 no autosave renaming takes place.
|
||||
|
||||
Defaults to 5.
|
||||
|
||||
|
||||
### factorio.enable_save_patching
|
||||
|
||||
Whether to use save patching or not.
|
||||
When enabled lua code is patched into the game save before starting it.
|
||||
Most plugins require the use of save patching to function.
|
||||
Turning it off makes it possible to use Clusterio to run and manage regular vanilla games and scenarios not compatible with Clusterio.
|
||||
|
||||
Defaults to true.
|
||||
|
||||
|
||||
### factorio.enable_whitelist
|
||||
|
||||
Turn on the whitelist on the server.
|
||||
|
||||
Defaults to false.
|
||||
|
||||
|
||||
### factorio.settings
|
||||
|
||||
Object with the settings to put into `server-settings.json` for the Factorio server.
|
||||
The settings in `data/server-settings.example.json` of the Factorio installation is used as the base and properties specified here overrides the properties there.
|
||||
|
||||
Changes to the following properties will be applied live if the instance is running while it is changed: `afk_autokick_interval`, `allow_commands`, `autosave_interval`, `autosave_only_on_server`, `description`, `ignore_player_limit_for_returning_players`, `max_players`, `max_upload_slots`, `max_upload_in_kilobytes_per_second`, `name`, `only_admins_can_pause_the_game`, `game_password`, `require_user_verification`, `tags`, `visibility`.
|
||||
|
||||
Defaults to {"tags":["clusterio"],"auto_pause":false}.
|
||||
|
||||
|
||||
### factorio.verbose_logging
|
||||
|
||||
Pass the `--verbose` flag to Factorio when starting the server.
|
||||
This prints more messages to the log.
|
||||
|
||||
Defaults to false.
|
||||
|
||||
|
||||
### factorio.strip_paths
|
||||
|
||||
Strip down absolute paths in the server log going to files inside the instance directory such that they are relative to the instance directory.
|
||||
This improves the signal to noise ratio of the log.
|
||||
|
||||
Sample output when disabled:
|
||||
|
||||
0.487 Loading map /srv/clusterio/instances/test/saves/world.zip: 2615888 bytes.
|
||||
0.614 Checksum for script /srv/clusterio/instances/test/temp/currently-playing/control.lua: 2390553941
|
||||
1.277 Script @/srv/clusterio/instances/test/temp/currently-playing/modules/example/test.lua:7: Example log line.
|
||||
|
||||
Sample output when enabled:
|
||||
|
||||
0.487 Loading map saves/world.zip: 2615888 bytes.
|
||||
0.614 Checksum for script temp/currently-playing/control.lua: 2390553941
|
||||
1.277 Script @modules/example/test.lua:7: Example log line.
|
||||
|
||||
Defaults to true.
|
||||
|
||||
### factorio.sync_adminlist
|
||||
|
||||
Synchronize in-game admin list with admin status of the users in cluster.
|
||||
If enabled Clusterio will generate a new `server-adminlist.json` on instance startup based on the users in the cluster with admin status set to true, and keep the admins in sync with the users that have admin status while the instance is running.
|
||||
|
||||
Defaults to true.
|
||||
|
||||
|
||||
### factorio.sync_whitelist
|
||||
|
||||
Synchronize in-game whitelist with the whitelisted status of the users in the cluster.
|
||||
If enabled Clusterio will generate a new `server-whitelist.json` on instance startup based on the users in the cluster with whitelisted status set to true, and keep the whitelisted users in sync with the users that have whitelisted status while the instance in running.
|
||||
|
||||
Defaults to true.
|
||||
|
||||
|
||||
### factorio.sync_banlist
|
||||
|
||||
Synchronize in-game banlist with the banned status of users in the cluster.
|
||||
If enabled Clusterio will generate a new `server-banlist.json` on instance startup based on users in the cluster with banned status set to true, and keep the banned users in sync with the users that have the banned status while the instance is running.
|
||||
|
||||
Defaults to true.
|
||||
|
||||
|
||||
### factorio.max_concurrent_commands
|
||||
|
||||
Maximum number of commands being transmitted into the game in parallel.
|
||||
Since the rate at which long commands are streamed into the game is by default limeted to 100 bytes/tick and scales down to 25 bytes/tick when there's 20 players connected, long commands can hold up the command interface and cause large latencies in getting data pushed into the game.
|
||||
(The rates are controlled by the `segment_size` options in the server settings.)
|
||||
To counteract this Clusterio sends up to this configured value number of commands in parallel over the RCON interface.
|
||||
This causes the game updates sent to clients to have this many command streams in parallel contained in them, drastically increasing the maxiumum size they can become.
|
||||
Since commands are only processed so ofter the maximum rate of commands that can be sustained is roughly 30 times this configured value per second, though note that large commands will lower this value.
|
||||
|
||||
The game updates sent by the server is split into roughly 500 byte packets, and if any one of those parts are lost the entire update is resent.
|
||||
This means a high number of concurrent commands can amplify resends due to packet loss and degrade the player experience up to the point of becomming completely unplayable.
|
||||
|
||||
Prior to 0.18.7 the maximum practical size of game update sent where arround 8 kB due to the UDP receive buffer on windows defaulting to this value, larger updates would not get through the receive buffer and cause the connection to drop.
|
||||
To stay below the 8 kB limit the maximum safe value for this option would be around 7000 / (3 * game_speed * maximum_segment_size), which is around 20 in normal circumstances.
|
||||
|
||||
Defaults to 5.
|
||||
|
||||
|
||||
### <plugin_name>.load_plugin
|
||||
|
||||
Whether to load the given plugin on the instance.
|
||||
Note that plugins need to be loaded on the controller in order for them to be loaded on instances.
|
||||
|
||||
Defaults to true.
|
||||
|
||||
|
||||
## Control Configuration
|
||||
|
||||
### control.controller_url
|
||||
|
||||
URL to connect to the controller to.
|
||||
|
||||
Defaults to null meaning complain about it not being set and exit.
|
||||
|
||||
|
||||
### control.controller_token
|
||||
|
||||
Access token used for authenticating with the controller.
|
||||
You can generate an access token with `clusteriocontroller bootstrap generate-user-token <username>`, or use the `clusteriocontroller bootstrap create-ctl-config <username>` to create a new ctl config with the correct url and token in it.
|
||||
|
||||
Defaults to null meaning complain about it not being set and exit.
|
||||
|
||||
|
||||
### control.tls_ca
|
||||
|
||||
Path to certificate in PEM format to use for validating a TLS connection to the controller.
|
||||
If you have a self signed certificate on the controller you will need to copy the certificate to the computer you run clusterctl from and set it as the certificate authority with this option.
|
||||
|
||||
Defaults to null meaning use Node.js's default set of trusted certificate authorities.
|
||||
|
||||
|
||||
### control.max_reconnect_delay
|
||||
|
||||
Max duration in seconds to wait before attempting to reconnect with the controller after the connection is dropped.
|
||||
The actual delay on each reconnect will be a random number between 0 and this configured value to avoid all clients trying to reconnect at the same time.
|
||||
|
||||
Defaults to 60.
|
||||
@@ -1,294 +0,0 @@
|
||||
# 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.
|
||||
We have included all the pratical considerations in this document.
|
||||
Although, we recomend checking out out our [past decisions](/docs/decisions.md) if you have the time.
|
||||
|
||||
|
||||
## 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)
|
||||
- [Publishing releases to npm](#publishing-releases-to-npm)
|
||||
- [Changelog](#changelog)
|
||||
- [Supported Node.js and Factorio Version](#supported-node.js-and-factorio-version)
|
||||
- [Code Style](#code-style)
|
||||
- [Naming Style](#naming-style)
|
||||
- [Units](#units)
|
||||
- [Indenting](#indenting)
|
||||
- [Line Length](#line-length)
|
||||
- [Markdown](#markdown)
|
||||
|
||||
|
||||
## 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/clusterio
|
||||
|
||||
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 tied together using pnpm's workspace feature.
|
||||
In order to get the development environment up and running you will need to run the following command:
|
||||
|
||||
pnpm install
|
||||
|
||||
This installs dependencies needed by tests and links the packages up so they work from the git work tree.
|
||||
To start a cluster from the repo you will first need to initialize it, this can be done using the installer:
|
||||
|
||||
node packages/create --dev
|
||||
|
||||
To add plugins to use in the development environment use
|
||||
|
||||
node packages/ctl plugin add ./path/to/plugin
|
||||
|
||||
It's important that the path starts with `./`.
|
||||
The `plugins/` directory contains the core plugins for the project and can be added using the above mentioned command.
|
||||
|
||||
The documentation uses the binaries provided by the packages, but for the development environment you will need to call the packages directly:
|
||||
|
||||
npx clusteriocontroller -> node packages/controller
|
||||
npx clusteriohost -> node packages/host
|
||||
npx clusterioctl -> node packages/ctl
|
||||
|
||||
By default node does not enable source maps, to get stack traces pointing to the `.ts` files you will need to pass `--enable-source-maps` to `node` or set the environment variable `NODE_OPTIONS=--enable-source-maps`.
|
||||
|
||||
Note that to use `clusterioctl` you will have to create a control config first:
|
||||
|
||||
node packages/controller bootstrap create-ctl-config [admin-user]
|
||||
|
||||
Once you've set up the cluster you can use `node packages/controller run` to run the controller and `node packages/host run` to run the host which connects to the controller.
|
||||
|
||||
When editing TypeScript files ran by Node.js you need to run `pnpm build` to compile the files to JavaScript before restarting the controller/host/ctl for changes take effect.
|
||||
To have TypeScript watch for changes and rebuild immediadly you may run `pnpm watch` instead.
|
||||
|
||||
For web development on the controller there are also the following flags:
|
||||
|
||||
`--dev` - Recompile the `web_ui` package on the fly providing live reloading of it.
|
||||
|
||||
`--dev-plugin [name]` - Recompile the web module for the given plugin on the fly proving live reloading of it.
|
||||
|
||||
These options can be combined and multiple plugins can be actived for dev mode.
|
||||
Keep in mind if you're working on external plugins the `--dev-plugin` option is likely to only work if the plugin is placed inside the `external_plugins` directory and its dependencies are installed with pnpm as part of the clusterio repository workspace.
|
||||
Note that webpack is configured to delete the web interface build when using these options, so you will have to run `pnpm run -r --if-present prepare` before you start the controller without these flags again.
|
||||
|
||||
|
||||
### 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 branch of the main repository.
|
||||
To do this check out that branch from the main repository.
|
||||
|
||||
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:
|
||||
|
||||
pnpm run test
|
||||
pnpm 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
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
|
||||
### Publishing releases to npm
|
||||
|
||||
For publishing new releases see [package release workflow](./release-workflow.md#packages).
|
||||
|
||||
## 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. Please make sure to document any [breaking changes](/docs/decisions.md#breaking-changes).
|
||||
|
||||
|
||||
## Supported Node.js and Factorio Versions
|
||||
|
||||
Clusterio run on Node.js v18 and up, make sure to check that the Node builtin API's and npm packages you use are supported on these versions.
|
||||
The GitHub Actions tests are run on whatever version of Node.js v18 and v20 is the default.
|
||||
|
||||
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 GitHub Actions tests are run against the latest experimental release.
|
||||
|
||||
Check out all our of decisions for [node, factorio, and operation support](/docs/decisions.md#supportability).
|
||||
|
||||
|
||||
## 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.
|
||||
See our [decisions document](/docs/decisions.md) for justifcations.
|
||||
|
||||
### 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 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.
|
||||
|
||||
Relative imports in 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("./link");
|
||||
const libLuaTools = require("./lua_tools");
|
||||
|
||||
Note that no sub-modules of the lib package is imported directly from the outside of it with the exception of `build_mod`.
|
||||
|
||||
For lua code lowercase_underscore is used for everything.
|
||||
|
||||
### Units
|
||||
|
||||
When naming variables whos unit is not a base SI unit like seconds or metres the unit used should be suffixed in the variable name:
|
||||
E.g.:
|
||||
|
||||
const startTimeMs = date.now();
|
||||
const endNs = process.hrtime();
|
||||
|
||||
This makes it easier to spot different unit bases are being mixed in the code.
|
||||
|
||||
|
||||
### 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 and Lua code should be indented with tabs.
|
||||
|
||||
Other files types should prefer tabs for indents where reasonable with the exception of Markdown.
|
||||
|
||||
|
||||
### 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
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
### Markdown
|
||||
|
||||
Markdown files are formatted using one line per sentence to improve diff reports when making changes.
|
||||
Headings have two spaces before them and on space after them to make them stand out, the heading at the top of the file does not have any spaces before it, and heading immediadly followed by another heading have one space between them.
|
||||
For example:
|
||||
|
||||
```md
|
||||
# A markdown File
|
||||
|
||||
Sentence of text which end in a period.
|
||||
Each sentence is on it's own line.
|
||||
And there's two spaces before the next heading.
|
||||
|
||||
|
||||
## Subheading
|
||||
|
||||
### Sub-subheading
|
||||
|
||||
Headings use the `# heading` format.
|
||||
```
|
||||
@@ -1,479 +0,0 @@
|
||||
# Decisions
|
||||
|
||||
This document outlines key decisions taken by the project team that should be followed by all maintainers and contributors.
|
||||
If there is a particular issue or concern you would like the team to comment on then please create a github issue for it.
|
||||
|
||||
<!-- Try to use categories that already exist if possible -->
|
||||
<!-- Sublists should be alphabetical, avoiding starting with "the" -->
|
||||
<!-- For code style, add (Lang) to section names if present in both -->
|
||||
|
||||
1. Documentation
|
||||
- [Decision Log](#decision-log)
|
||||
- [TSDoc](#tsdoc)
|
||||
2. Code Style (General)
|
||||
- [Indenting](#indenting)
|
||||
- [Line Length](#line-length)
|
||||
- [Naming Booleans](#naming-booleans)
|
||||
- [Naming Files](#naming-files)
|
||||
- [Naming Times and Durations](#naming-times-and-durations)
|
||||
- [Strings](#strings)
|
||||
3. Code Style (TS / JS / Web)
|
||||
- [ESLint](#eslint)
|
||||
- [Imports Within `lib` Package](#imports-within-lib-package)
|
||||
- [Name Style](#name-style-ts)
|
||||
4. Code Style (Lua)
|
||||
- [FMTK](#fmtk)
|
||||
- [Name Style](#name-style-lua)
|
||||
5. Code Style (Other)
|
||||
- [Markdown](#markdown)
|
||||
6. Repository Structure
|
||||
- [External Plugins](#external-plugins)
|
||||
- [First Party Plugins](#first-party-plugins)
|
||||
- [Core Packages](#core-packages)
|
||||
- [TypeScript Configs](#typescript-configs)
|
||||
- [Workspace](#workspace)
|
||||
7. Project Architecture
|
||||
- [Deprecation of Phin in favour of `fetch`](#deprecation-of-phin-in-favour-of-fetch)
|
||||
- [Minimal Dependencies](#minimal-dependencies)
|
||||
- [React](#react)
|
||||
- [Separation of Host and Controller](#separation-of-host-and-controller)
|
||||
- [Typescript](#typescript)
|
||||
- [Webpack](#webpack)
|
||||
8. Supportability
|
||||
- [Breaking Changes](#breaking-changes)
|
||||
- [Distribution](#distribution)
|
||||
- [Factorio](#factorio)
|
||||
- [General Support](#general-support)
|
||||
- [NodeJS](#nodejs)
|
||||
- [Operating System](#operating-systems)
|
||||
9. Testing
|
||||
- [Continuous Integration](#continuous-integration)
|
||||
- [Continuous Deployment](#continuous-deployment)
|
||||
- [Mocha](#mocha)
|
||||
- [Version Matrix](#version-matrix)
|
||||
|
||||
> [!note]
|
||||
> The use of `Time Immemorial` indicates an obsverion of a previously undocumented decision with no known start date, which has remained unchallenged.
|
||||
|
||||
<!--
|
||||
### Template
|
||||
|
||||
🧾 0th Month 2000 | Source
|
||||
|
||||
> A short sentence describing the action to take in the context of our project.
|
||||
|
||||
Write a breif description which includes: the reason a decision was needed, the outcome of the discussion, and the justification of the outcome.
|
||||
Write using the active voice in third person "we".
|
||||
Remember to have one sentence per line.
|
||||
-->
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
### Decision Log
|
||||
|
||||
🧾 22nd December 2024 | Discord Voice Call
|
||||
|
||||
> Maintain a document that includes a brief description of the outcomes and the reasoning behind our decisions, along with links to the relevant sources for further reading.
|
||||
|
||||
We discovered that we were beginning to ask similar questions to other maintainers that we had previously raised, which led to a waste of time for all involved.
|
||||
Consequently, we decided that a log of important decisions should be kept for the benefit of ourselves and future maintainers.
|
||||
|
||||
|
||||
### TSDoc
|
||||
|
||||
🧾 12th August 2023 | [Discord Discussion](https://discord.com/channels/450361298220351489/450361298220351491/1320573126731370506) | [Github PR #493](https://github.com/clusterio/clusterio/pull/493)
|
||||
|
||||
> When a function or it's arguments do not have a clear purpose, a TSDoc comment should be used to describe it.
|
||||
|
||||
While TypeScript provides useful type infomation in function hover text, it does not allow for comments about the purpose of functions and their arguments.
|
||||
Therefore, we decided to use TSDoc comments for this purpose as it is well intergrated in many text editors and IDEs (such as VSCode).
|
||||
Our specific implementation is TypeDoc because it is lightweight it and can output TSDoc comments as html.
|
||||
API Extractor was also considered but not selected.
|
||||
|
||||
|
||||
## Code Style (General)
|
||||
|
||||
### Indenting
|
||||
|
||||
🧾 Time Immemorial
|
||||
|
||||
> Use tabs for indention. Except for Markdown where spaces carry meaning.
|
||||
|
||||
We needed to maintain a consistent whitespace style across all our files.
|
||||
Therefore, we selected tabs as our standard.
|
||||
|
||||
|
||||
### Line Length
|
||||
|
||||
🧾 Time Immemorial
|
||||
|
||||
> Lines are limited to 120 characters where tabs count as 4 characters.
|
||||
|
||||
We needed to maintain a consistent max line legnth across all our files.
|
||||
Therefore, we selected 120 characters as our standard.
|
||||
|
||||
|
||||
### Naming Booleans
|
||||
|
||||
🧾 25th November 2024 | [Discord Discussion](https://discord.com/channels/450361298220351489/450361298220351491/1310611960508977223)
|
||||
|
||||
> Variable and member properties which contain booleans should be named starting with a verb unless it ends with "ed", e.g. `canRestart` `expectEmpty` `hasFallback` `connected` `enabled` `loaded` `banned`
|
||||
|
||||
We had inconsistencies in our naming of booleans and aimed to standardise them.
|
||||
There was little preference for any particular approach, so we chose the one that would require the fewest changes to existing code.
|
||||
As a result, all boolean values should begin with a verb, unless they end with "ed." For clarity, both "is" and "has" are considered verbs.
|
||||
|
||||
|
||||
### Naming Files
|
||||
|
||||
🧾 Time Immemorial
|
||||
|
||||
> Files use lowercase_underscore if multiple values are exported. If a single class is exported then it uses the class name in PascalCase.
|
||||
|
||||
We needed to maintain a consistent name style across all our files.
|
||||
Therefore, we selected lowercase undercore for most files, with class files using PascalCase.
|
||||
|
||||
|
||||
### Naming Times and Durations
|
||||
|
||||
🧾 18th January 2024 | [Discord Discussion](https://discord.com/channels/450361298220351489/450361298220351491/1197566542548828170)
|
||||
|
||||
> Variable and member properties which contain times or durations should end with their SI unit of messure, e.g. `updatedAtMs`
|
||||
|
||||
We found verifying the units of measurement used in different locations to be a simple yet repetitive task that could lead to errors.
|
||||
Our main concern was the mixing of variables containing seconds and milliseconds.
|
||||
Therefore, we decided that times and durations should explicitly include their units, whereas other value types do not require any units.
|
||||
|
||||
|
||||
### Strings
|
||||
|
||||
🧾 Time Immemorial
|
||||
|
||||
> Strings should use double quotes `"` unless the string contains them, in which case single quotes `'` can be used. For JS string templates use backticks ``` ` ``` as normal.
|
||||
|
||||
We needed to maintain a consistent string style across all our files.
|
||||
Therefore, we selected double quotes as our standard.
|
||||
|
||||
## Code Style (TS / JS / Web)
|
||||
|
||||
### ESLint
|
||||
|
||||
🧾 Time Immemorial
|
||||
|
||||
> Use ESLint for style checking.
|
||||
|
||||
We needed to maintain a consistent style across all our files.
|
||||
Therefore, we utilise the industry-standard ESLint, as it is familiar and highly configurable.
|
||||
|
||||
|
||||
### Imports Within `lib` Package
|
||||
|
||||
🧾 Time Immemorial
|
||||
|
||||
> When importing files within the lib package, all names should be prefixed with "lib".
|
||||
|
||||
The exports of the various library files are often very generic and may conflict with variable names.
|
||||
Therefore, when importing these files the "lib" prefix should be used to avoid confussing.
|
||||
This only applies to files within `lib` importing other files in `lib` because all other packages import the whole of `lib` as a single package.
|
||||
|
||||
|
||||
### Name Style (TS)
|
||||
|
||||
🧾 Time Immemorial
|
||||
|
||||
> Variables and members of classes use camelCase. Classes use PascalCase. Config values use lowercase_underscore. Exceptions allowed for over the write objects.
|
||||
|
||||
We needed to maintain a consistent name style across all our files.
|
||||
Therefore, we follow common practive for JavaScript of using camelCase and PascalCase.
|
||||
Although, we have also allowed the use of lowercase_underscore for config values to distinguish them from other variables.
|
||||
|
||||
|
||||
## Code Style (Lua)
|
||||
|
||||
### FMTK
|
||||
|
||||
🧾 14th June 2024 | [External PR](https://github.com/justarandomgeek/vscode-factoriomod-debug/pull/126)
|
||||
|
||||
> Use of FMTK is not required but is advised.
|
||||
> To enable Clusterio support, use the CLI flag `--clusterio-modules` under `Lua.runtime.pluginArgs`.
|
||||
|
||||
We needed to maintain a consistent style across all our files.
|
||||
There are multiple competing standards for Lua code, and given the limited amount of Lua code in this project, we chose not to adopt any automatic linting or shared luarc file.
|
||||
Nevertheless, we recomend the use of [FMTK](https://github.com/justarandomgeek/vscode-factoriomod-debug) and have taken steps towords supporting it in Clusterio.
|
||||
|
||||
|
||||
### Name Style (Lua)
|
||||
|
||||
🧾 Time Immemorial
|
||||
|
||||
> Everything uses lowercase_underscore
|
||||
|
||||
We needed to maintain a consistent name style across all our files.
|
||||
Therefore, we follow common practive for Lua of using lowercase_underscore.
|
||||
|
||||
|
||||
## Code Style (Other)
|
||||
|
||||
### Markdown
|
||||
|
||||
🧾 Time Immemorial
|
||||
|
||||
> Use one sentence per line.
|
||||
> Place two lines before each heading, except for the first one or when two headings are adjacent.
|
||||
|
||||
We needed to maintain a consistent code style across all our markdown files.
|
||||
Therefore, we selected an approach which makes diff reports more useful and which makes headings use additional space to make them stand out.
|
||||
|
||||
|
||||
## Repository Structure
|
||||
|
||||
### External Plugins
|
||||
|
||||
🧾 Time Immemorial
|
||||
|
||||
> No external plugins should be included in the main repository.
|
||||
> A folder will exist which external plugin repositories can be cloned into and will be given the same treatment as first party plugins.
|
||||
|
||||
We require a designated space for developers to create their plugins without the concern of conflicts with first-party plugins.
|
||||
To address this, we have introduced a dedicated folder for external plugins.
|
||||
This solution is straightforward and has since proven to be highly effective.
|
||||
|
||||
|
||||
### First Party Plugins
|
||||
|
||||
🧾 Time Immemorial
|
||||
|
||||
> First party plugins are included in the main repository.
|
||||
> Those selected to be first party are held to the same stanards which apply to the core packages.
|
||||
> Each will recieve a new release in step with the core packages.
|
||||
|
||||
We require a designated space for first-party plugins, separate from the core packages, to clearly represent the dependency between them.
|
||||
Creating a folder provided a simple solution to this.
|
||||
|
||||
|
||||
### Core Packages
|
||||
|
||||
🧾 Time Immemorial
|
||||
|
||||
> All core packages will be contained in the packages folder of the repository.
|
||||
> They are allowed to be interdependent on each other.
|
||||
|
||||
We require a set of core packages to represent the different applications used in a cluster.
|
||||
See [Separation of Host and Controller](#separation-of-host-and-controller).
|
||||
To avoid putting more files into the repository root we created a folder for the core packages.
|
||||
|
||||
|
||||
### TypeScript Configs
|
||||
|
||||
🧾 26th August 2023 | [Github PR #504](https://github.com/clusterio/clusterio/pull/504)
|
||||
|
||||
> Each package / plugin may contain any of: `tsconfig.json` `tsconfig.node.json` and `tsconfig.web.json`.
|
||||
> The main respository root will contain all three files in addition to `tsconfig.base.json` which node and web extend from.
|
||||
> The configs inside packages / plugins should extend from those in the respository root.
|
||||
|
||||
We found that maintaining independent tsconfig files became burdensome and required a way to consolidate them.
|
||||
Initially, we introduced a base configuration file; however, this proved insufficient, so we extended it to include separate configurations for Node.js and the Web.
|
||||
This approach led to each package having up to three tsconfig files.
|
||||
Nevertheless, we believe this to be justified, given the need to differentiate between Node.js and Web builds while maintaining a single, shared configuration.
|
||||
|
||||
|
||||
### Workspace
|
||||
|
||||
🧾 Time Immemorial
|
||||
|
||||
> The workspace root should contain only: configuration files, scripts used to build / clean the workspace, the readme, and the changelog.
|
||||
|
||||
We need to maintain a clean workspace root by only including files which effect all pacakges and plugins such as configuration files.
|
||||
We have allowed for build and clean scripts to exist here as long as they are not published as part of any package or plugin.
|
||||
|
||||
|
||||
## Project Architecture
|
||||
|
||||
### Deprecation of Phin in favour of `fetch`
|
||||
|
||||
🧾 12th December 2024 | [Discord Discussion Dec 24](https://discord.com/channels/450361298220351489/450361298220351491/1316781609206419567) | [Discord Discussion May 25](https://discord.com/channels/450361298220351489/450361298220351491/1240246997496102942)
|
||||
|
||||
> The builtin `fetch` method should be used as a replacement of the deprecated Phin module.
|
||||
|
||||
We required a replacement for the deprecated Phin HTTP client, which was used for making server-side requests and as part of testing.
|
||||
With `fetch` being marked as stable in Node.js v21.0.0 and having first been made available in v16.15.0, it serves as a suitable replacement without the need to add new dependencies.
|
||||
It had previously not been used because it was not marked as stable in any LTS version of NodeJS.
|
||||
|
||||
|
||||
### Minimal Dependencies
|
||||
|
||||
🧾 Time Immemorial
|
||||
|
||||
> Any new dependencies need to be well justified.
|
||||
|
||||
We aim to minimise our exposure to upstream changes by using a minimal number of dependencies.
|
||||
This also serves to reduce the bloat of a cluster installation and improve installation times.
|
||||
Consequently, the addition of any new dependencies must be thoroughly justified, with evidence of research into alternative options and comparisons between them.
|
||||
|
||||
|
||||
### React
|
||||
|
||||
🧾 Time Immemorial
|
||||
|
||||
> Our web stack uses ReactJS
|
||||
|
||||
We require a reactive website for our control panel, and ReactJS was selected due to its widespread use and familiarity.
|
||||
Switching away from React for any reason would represent a significant undertaking.
|
||||
|
||||
|
||||
### Separation of Host and Controller
|
||||
|
||||
🧾 Time Immemorial
|
||||
|
||||
> The primary application locations are Control (web and cli), Controller, and Host.
|
||||
> Therefore the core packages are: `ctl` `controller` `host`. There are two libaray packages `lib` and `web_ui`.
|
||||
> Additionally `create` exists to provide easy creation of a cluster.
|
||||
|
||||
A single application to perform all the duties of a cluster would complicate the reasoning and maintenance of the project, so we decided to split it into several packages.
|
||||
The primary components of a cluster include servers connected to an instance, which runs on a host that connects to the controller, whose configuration is managed via a control connection from a CLI or web portal.
|
||||
Based on this structure, we found it logical to divide the packages according to their installation location, resulting in `ctl`, `controller`, and `host`, with shared code separated into `lib` and `web_ui` depending on where it is utilised.
|
||||
The additional `create` package is used by `npm create` to provide a user-friendly installation process.
|
||||
|
||||
|
||||
### TypeScript
|
||||
|
||||
🧾 6th August 2023 | [Github PR #491](https://github.com/clusterio/clusterio/issues/491)
|
||||
|
||||
> We use TypeScript as our language of choice.
|
||||
|
||||
We previously used JavaScript and switched to TypeScript to provide a robust type system.
|
||||
Transitioning to any other langauge would be too significant an undertaking.
|
||||
Initially, we supported multiple languages for plugins, including Rust and Go; however, this approach was quickly abandoned due to its complexity.
|
||||
|
||||
|
||||
### Webpack
|
||||
|
||||
🧾 Time Immemorial
|
||||
|
||||
> Our web stack uses Webpack and associated plugins.
|
||||
|
||||
We required a bundler for our web build, and at the time, Webpack was the standard.
|
||||
While we have considered alternatives since, transitioning to another solution would be a significant undertaking.
|
||||
|
||||
|
||||
## Supportability
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
🧾 Time Immemorial
|
||||
|
||||
> We follow symver versioning for breaking changes.
|
||||
> In our alpha stage, we allow every alpha to contain breaking changes as long as their are documented and provide migration actions.
|
||||
> Once we enter beta, breaking changes will be more restricted.
|
||||
|
||||
We aim to be highly transparent with breaking changes and maintain compatibility with as many existing installations as possible.
|
||||
However, as we are currently in the alpha stage, things are unstable, and breaking changes occur frequently.
|
||||
As such, we have decided to document all breaking changes and provide migration steps where the changes are non-trivial.
|
||||
Once we enter the beta stage, we will implement stricter restrictions on breaking changes.
|
||||
|
||||
|
||||
### Distribution
|
||||
|
||||
🧾 Time Immemorial
|
||||
|
||||
> We use `git` and `pnpm` for development distribution.
|
||||
> We use `npm` and the factorio mod portal for production distribution.
|
||||
|
||||
We need to distribute both production builds and development source code for the project.
|
||||
By necessity, we use `npm` and the Factorio Mod Portal to distribute our production builds.
|
||||
For development, we rely on `git` and GitHub, which are industry standards, while using `pnpm` as a more efficient variant of `npm` for development installations.
|
||||
Although bun was considered upon its release, we were not convinced it was the right choice.
|
||||
|
||||
|
||||
### Factorio
|
||||
|
||||
🧾 26th October 2024 | [Discord Discussion](https://discord.com/channels/450361298220351489/450361298220351491/1299637042350653461)
|
||||
|
||||
> We actively provide full support for the last two major versions of Factorio.
|
||||
> This approach does not follow semantic versioning; instead, it follows the "middle" versions: 2.1 > 2.0 > 1.1 > 1.0 > 0.18 > 0.17.
|
||||
> We offer passive support for all versions from 0.17 onwards, where implementation is trivial and does not impose restrictions on our actively supported versions.
|
||||
|
||||
Following the 2.0 update of Factorio, which introduced a large number of breaking changes, we needed to reconsider which versions would be actively supported, as it was no longer trivial to support all versions.
|
||||
Anticipating that future breaking changes would also be significant, and assuming the majority of players would update to the latest stable version upon release, we decided to support the latest stable version and the previous major version that introduced breaking changes.
|
||||
Support for other versions will not be removed if they have already been implemented and do not impose restrictions on our ability to support newer versions.
|
||||
|
||||
|
||||
### General Support
|
||||
|
||||
🧾 Time Immemorial
|
||||
|
||||
> We provide full support via GitHub and our Discord server for the latest published version of Clusterio, as well as assistance with migrating from the previous published version.
|
||||
> However, we do not offer any guarantees with our support; we assist when time permits and may not be able to resolve every issue.
|
||||
> Additionally, while we offer support in setting up development workspaces and guidance for plugin development, we do not provide support for those running development builds as production instances.
|
||||
|
||||
We aim to support as many users of Clusterio as possible in setting up and using the platform for a variety of purposes, including setting up production instances, development workspaces, and providing guidance on plugin development.
|
||||
However, our time to work on Clusterio is limited, and even more so our ability to offer support.
|
||||
As such, we cannot assist with every issue and must set reasonable restrictions to ensure expectations are aligned.
|
||||
Consequently, we do not offer any guarantees for our support, but we strive to provide as much assistance as possible for those setting up or running the latest published version.
|
||||
Additionally, we support users in setting up development workspaces, as the more people willing to contribute, the better.
|
||||
|
||||
|
||||
### NodeJS
|
||||
|
||||
🧾 Time Immemorial
|
||||
|
||||
> We support all LTS versions of NodeJS in their active and maintenance phases.
|
||||
|
||||
Our options were: supporting all versions, supporting all active versions, supporting all LTS versions, or supporting only the latest version.
|
||||
As we aim to provide a stable, production-ready tool while still limiting the number of versions we need to manage, we chose to support all LTS versions because NodeJS can be installed by the majority of linux distro package managers which will always offer at least one LTS version.
|
||||
|
||||
|
||||
### Operating Systems
|
||||
|
||||
🧾 Time Immemorial
|
||||
|
||||
> We support all major versions of Windows, Linux, and MacOS.
|
||||
This is largely inpart to our use of NodeJS and is not a guarantee from us.
|
||||
|
||||
Our choice of Node.js has enabled us to support all major operating systems with minimal effort on our part.
|
||||
Nevertheless, there have been instances where code paths have diverged between systems, and we have attempted to maintain support for as many systems as possible.
|
||||
However, this is not a priority, and such support has often been reactive or provided by external contributors.
|
||||
|
||||
|
||||
## Testing
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
🧾 Time Immemorial
|
||||
|
||||
> We run unit and integration tests through GitHub CI actions as well as test coverage through CodeCov.
|
||||
|
||||
Continuous integration actions in GitHub are a valuable tool that we use to assist with the review of pull requests from both maintainers and contributors.
|
||||
While we aim to increase the number of useful checks performed by CI in the future, this is not a priority over the development of new features.
|
||||
|
||||
|
||||
### Continuous Deployment
|
||||
|
||||
🧾 Time Immemorial
|
||||
|
||||
> We do not use any CD actions, and have no plans of implementing them.
|
||||
|
||||
Unlike CI, we do not find continuous deployment useful at our current stage of the project.
|
||||
This is due to the frequent breaking changes merged into the main branch, which we prefer to bundle together as a single version.
|
||||
In the future, we may automate the deployment process, but it will always require manual activation.
|
||||
|
||||
|
||||
### Mocha
|
||||
|
||||
🧾 Time Immemorial
|
||||
|
||||
> We use Mocha for testing and CodeCov for coverage.
|
||||
|
||||
We chose Mocha because it was a good fit when we selected it.
|
||||
However, we are always looking for a better framework which is worth the effort of switching.
|
||||
|
||||
|
||||
### Version Matrix
|
||||
|
||||
🧾 Time Immemorial
|
||||
|
||||
> We perform all tests through GitHub actions using Ubuntu latest, using all supported versions of node and all supported versions of factorio.
|
||||
|
||||
We perform tests on all versions of NodeJS and Factorio that we support.
|
||||
However, we do not conduct tests on all supported operating systems, as the added demand on our testing resources would be significant, and the value it would provide for a cross-platform engine like NodeJS would be limited.
|
||||
@@ -1,297 +0,0 @@
|
||||
# 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
|
||||
|
||||
Clusterio uses a homebrew protocol based on sending JSON payloads over a WebSocket connection.
|
||||
See [socket.md](socket.md) for the implementation details of it.
|
||||
|
||||
It's also possible to write a plugin for Clusterio that exposes a custom interface over HTTP, WebSocket or any other technology supported by Node.js.
|
||||
|
||||
|
||||
## 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.
|
||||
Like with scenarios for Clusterio the [event_handler interface](#event_handler-interface) has to be used for any event subscriptions in modules.
|
||||
|
||||
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.
|
||||
@@ -1,119 +0,0 @@
|
||||
# JSON Serialisable Classes
|
||||
|
||||
To serialise and deserialise data the Clusterio codebase uses a JSON serialisable data class pattern.
|
||||
Instances of these classes can be serialised to JSON by passing them directly to `JSON.serialise` and deserialised by passing the serialised JSON representation to its static `fromJSON` method.
|
||||
The data class also has a static `jsonSchema` property describing the serialised JSON representation for validation purposes.
|
||||
See the following example code for a hypothetical `Data` class with a single property `member` defaulting to a value of `"foo"`.
|
||||
|
||||
|
||||
```js
|
||||
// Data.js
|
||||
"use strictc";
|
||||
|
||||
/**
|
||||
* A Data class example
|
||||
* @alias module:Data
|
||||
*/
|
||||
class Data {
|
||||
/** @type {string} */
|
||||
member = "foo";
|
||||
|
||||
constructor(member) {
|
||||
if (member !== undefined) { this.member = member; }
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
"member": { type: "string" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.member);
|
||||
}
|
||||
|
||||
// toJSON is called by JSON.stringify when serialising this class
|
||||
toJSON() {
|
||||
let json = {};
|
||||
// Omit property if it's set to the default value
|
||||
if (this.member !== "foo") { json["member"] = this.member; }
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Data;
|
||||
```
|
||||
|
||||
Serialising an instance of `Data` for JSON is as simple as doing `serialised = JSON.stringify(data)` as the `toJSON` method is automatically called by `JSON.stringify` to create a suitable object.
|
||||
Deserialising is done by calling the static `fromJSON` method with the serialised JSON data, like `data = Data.fromJSON(JSON.parse(serialised))`.
|
||||
|
||||
By structuring the classes like this they can be easily composed together.
|
||||
For example a class holding a data property with an instance of the Data class from the previous example can be created as follows.
|
||||
|
||||
```js
|
||||
// Composed.js
|
||||
"use strict";
|
||||
const Data = require("./Data");
|
||||
|
||||
/**
|
||||
* Composed data class example
|
||||
* @alias module:Data
|
||||
*/
|
||||
class Composed {
|
||||
/** @type {module:Data|undefined} */
|
||||
data;
|
||||
|
||||
constructor(data) {
|
||||
if (data) { this.data = data; }
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
"data": Data.jsonSchema,
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
let data;
|
||||
if (json["data"]) { data = Data.fromJSON(json["data"]); }
|
||||
return new this(data);
|
||||
}
|
||||
|
||||
// toJSON is optional and not needed for this particular class
|
||||
}
|
||||
|
||||
module.exports = Composed;
|
||||
```
|
||||
|
||||
It's easy to create a new instance of Composed by calling `new Composed(new Data("spam"))` and serialising to and from JSON is the same as for Data.
|
||||
Meaning `serialised = JSON.stringify(composed)` and `composed = Composed.fromJSON(JSON.parse(serialised))` does as expected.
|
||||
|
||||
## Interface description
|
||||
|
||||
Data classes are ES6 style class declarations with the following methods and fields:
|
||||
|
||||
### toJSON(): any (optional)
|
||||
|
||||
Method returning an object or primitive suitable for passing to JSON.stringify in order to create a JSON representation of the class instance.
|
||||
The returned object may contain objects that have their own toJSON methods which JSON.stringify will then call recursively.
|
||||
|
||||
This method is optional if calling `JSON.stringify` on an instance without it would produce the desired serialised form, however this is rarely the case as often you'll have to provide it to convert the values fram Map/Set properites, omit default values or do similar actions.
|
||||
|
||||
Note that the primary use case for serialising data classes is to send them over the network, this method should therefore try to reduce the size of the resulting JSON by for example omitting properties who's values are the default value.
|
||||
|
||||
### static fromJSON(json: any): this
|
||||
|
||||
Method constructing an instance of this class from one that was previously serialised by passing an instance of it to `JSON.stringify`.
|
||||
|
||||
While not a requirement it is preferrable if the input is not mutated and that no references to any any structures in the input ends up in the returned object.
|
||||
In other words calling fromJSON twice with the same input should produce two independent objects.
|
||||
|
||||
Note that directly passing the output of `toJSON` to `fromJSON` is not valid, as the output of `toJSON` may require further expansion calling `toJSON` on objects returned by it.
|
||||
It has to go through `JSON.stringify` or an equivalent recursive expansion of `toJSON`.
|
||||
|
||||
### static jsonSchema
|
||||
|
||||
A [JSON schema](http://json-schema.org/) describing the format of the JSON resulting from passing an instance of this class to `JSON.stringify`.
|
||||
This should be lenient towards adding additional properties that would be ignored by `fromJSON` as it makes it easier to extend these classes in the future without breaking compatibility.
|
||||
@@ -1,523 +0,0 @@
|
||||
# Migrating Plugins to Alpha 14
|
||||
|
||||
Between version 2.0.0-alpha.13 and 2.0.0-alpha.14 the codebase underwent major refactoring and was ported to TypeScript.
|
||||
In order to support alpha 14 there are several major changes that need to be done to existing plugins, these are in no particular order
|
||||
|
||||
|
||||
## Rename master and slave
|
||||
|
||||
All references to slave and master in the code was renamed to controller and host respectively which means that code referencing these in plugins must be renamed.
|
||||
In short this means performing a global search and replace on the codebase replacing:
|
||||
|
||||
- `master` with `controller`
|
||||
- `Master` with `Controller`
|
||||
- `slave` with `host`
|
||||
- `Slave` with `Host`
|
||||
|
||||
in all places it occurs, even as part of longer names such as changing `BaseMasterPlugin` to `BaseControllerPlugin`.
|
||||
|
||||
|
||||
## Change lib imports to import directly from the top level package
|
||||
|
||||
Previously code could import sub packages of lib such as lib/lua_tools.
|
||||
These imports need to be changed to all import from lib directly.
|
||||
|
||||
**Alpha 13**
|
||||
```js
|
||||
const libLuaTools = require("@clusterio/lib/lua_tools");
|
||||
const libErrors = require("@clusterio/lib/errors");
|
||||
|
||||
// Using imports
|
||||
new libErrors.RequestError(...)
|
||||
libLuaTools.escapeString(...)
|
||||
```
|
||||
|
||||
**Alpha 14 (JS)**
|
||||
```js
|
||||
const lib = require("@clusterio/lib");
|
||||
|
||||
// Using library components
|
||||
new lib.RequestError(...)
|
||||
lib.escapeString(...)
|
||||
```
|
||||
|
||||
**Alpha 14 (TS)**
|
||||
```ts
|
||||
import * as lib from "@clusterio/lib";
|
||||
|
||||
// Using library components
|
||||
new lib.RequestError(...)
|
||||
lib.escapeString(...)
|
||||
```
|
||||
|
||||
|
||||
## Change import of base plugins
|
||||
|
||||
Previously `BaseMasterPlugin` and `BaseInstancePlugin` were imported from `@clusterio/lib`, these have been moved to `@clusterio/controller` and `@clusterio/host` respectively.
|
||||
|
||||
**Alpha 13**
|
||||
```js
|
||||
const { BaseMasterPlugin } = require("@clusterio/lib");
|
||||
const { BaseInstancePlugin } = require("@clusterio/lib");
|
||||
```
|
||||
|
||||
**Alpha 14 (JS)**
|
||||
```js
|
||||
const { BaseControllerPlugin } = require("@clusterio/controller");
|
||||
const { BaseInstancePlugin } = require("@clusterio/host");
|
||||
```
|
||||
|
||||
**Alpha 14 (TS)**
|
||||
```ts
|
||||
import { BaseControllerPlugin } from "@clusterio/controller";
|
||||
import { BaseInstancePlugin } from "@clusterio/host";
|
||||
```
|
||||
|
||||
|
||||
## Change main entrypoint of plugin
|
||||
|
||||
Previously the info of a plugin would be loaded by requiring `./plugin_dir/info`, this has been changed to require `./plugin_dir` which in turn means that `info.js` is no longer the default entrypoint.
|
||||
To solve this you can either add `"main": "info.js",` to package.json or rename `info.js` to `index.js` (the rest of this guide assumes the entrypoint was renamed to `index.js`).
|
||||
The motivation behind this change was to allow TypeScript to output its build to a sub-folder of the plugin, if you're using TypeScript you'll likely have the entrypoint be located in `dist/node/index.js` instead.
|
||||
|
||||
The expected place where the declaration of a plugin is exported has also changed from `module.exports` to `module.exports.plugin`:
|
||||
|
||||
**Alpha 13**
|
||||
```js
|
||||
// info.js
|
||||
module.exports = {
|
||||
name: "plugin_name",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**Alpha 14 (JS)**
|
||||
```js
|
||||
// index.js
|
||||
const plugin = {
|
||||
name: "plugin_name",
|
||||
...
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
plugin,
|
||||
};
|
||||
```
|
||||
|
||||
**Alpha 14 (TS)**
|
||||
```ts
|
||||
// index.ts
|
||||
import type * as lib from "@clusterio/lib";
|
||||
|
||||
export const plugin: lib.PluginDeclaration = {
|
||||
name: "plugin_name",
|
||||
...
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
### Modify exposed path in webpack.config.js
|
||||
|
||||
The `./info` import path exposed in the ModuleFederationPlugin in the `webpack.config.js` needs to be changed to `./`.
|
||||
E.g.:
|
||||
|
||||
**Alpha 13**
|
||||
```js
|
||||
// In webpack.config.js
|
||||
new webpack.container.ModuleFederationPlugin({
|
||||
...
|
||||
exposes: {
|
||||
"./info": "./info.js",
|
||||
...
|
||||
},
|
||||
}),
|
||||
```
|
||||
|
||||
**Alpha 14**
|
||||
```js
|
||||
// In webpack.config.js
|
||||
new webpack.container.ModuleFederationPlugin({
|
||||
...
|
||||
exposes: {
|
||||
"./": "./index.js",
|
||||
...
|
||||
},
|
||||
}),
|
||||
```
|
||||
|
||||
## Remove usage of ConfigGroup
|
||||
|
||||
The Config system was refactored and the `ConfigGroup` concept was completely removed.
|
||||
Config fields are now defined directly on the exported plugin info.
|
||||
|
||||
**Alpha 13**
|
||||
```js
|
||||
const lib = require("@clusterio/lib");
|
||||
|
||||
class MasterConfigGroup extends lib.PluginConfigGroup { }
|
||||
MasterConfigGroup.groupName = "plugin_name";
|
||||
MasterConfigGroup.defaultAccess = ["master", "control"];
|
||||
MasterConfigGroup.define({
|
||||
name: "option",
|
||||
title: "Option",
|
||||
description: "A config option.",
|
||||
type: "string",
|
||||
optional: true,
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
name: "plugin_name",
|
||||
MasterConfigGroup,
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**Alpha 14 (JS)**
|
||||
```js
|
||||
const plugin = {
|
||||
name: "plugin_name",
|
||||
controllerConfigFields: {
|
||||
"plugin_name.option": {
|
||||
title: "Option",
|
||||
description: "A config option.",
|
||||
type: "string",
|
||||
initial_value: "foo",
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
...
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
plugin,
|
||||
};
|
||||
```
|
||||
|
||||
**Alpha 14 (TS)**
|
||||
```ts
|
||||
import type * as lib from "@clusterio/lib";
|
||||
|
||||
// Declare the type of the field to make the type of controller.config.get(field) correctly deduced.
|
||||
declare module "@clusterio/lib" {
|
||||
export interface ControllerConfigFields {
|
||||
"plugin_name.option": null | string;
|
||||
}
|
||||
}
|
||||
|
||||
export const plugin: lib.PluginDeclaration = {
|
||||
name: "plugin_name",
|
||||
controllerConfigFields: {
|
||||
"plugin_name.option": {
|
||||
title: "Option",
|
||||
description: "A config option.",
|
||||
type: "string",
|
||||
initialValue: "foo",
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
...
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
### Use onControllerConfigFieldChanged instead of listering to the fieldChanged event
|
||||
|
||||
**Alpha 13**
|
||||
```js
|
||||
this.controller.config.on("fieldChanged", (group, field, prev) => {
|
||||
if (group.name === "plugin_name" && field === "option") {
|
||||
const curr = group.get(field);
|
||||
// Do stuff with curr and or prev
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Alpha 14 (JS)**
|
||||
```js
|
||||
async onControllerConfigFieldChanged(field, curr, prev) {
|
||||
if (field === "plugin_name.option") {
|
||||
// Do stuff with curr and or prev
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Alpha 14 (TS)**
|
||||
```ts
|
||||
async onControllerConfigFieldChanged(field: string, curr: unknown, prev: unknown) {
|
||||
if (field === "plugin_name.option") {
|
||||
// Do stuff with curr and or prev
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Redefine link messages as classes
|
||||
|
||||
The link layer has been majorly refactored and Request and Events are now defined as [JSON Serialisable Classes](./json-serialisable-classes.md).
|
||||
|
||||
**Alpha 13**
|
||||
```js
|
||||
module.exports = {
|
||||
name: "plugin_name",
|
||||
messages: {
|
||||
forwarded: new lib.Event({
|
||||
type: "plugin_name:forwarded",
|
||||
links: ["instance-slave", "slave-master"],
|
||||
forwardTo: "master",
|
||||
eventProperties: {
|
||||
"string_value": { type: "string" },
|
||||
"number_value": { type: "number" },
|
||||
},
|
||||
}),
|
||||
simple: new lib.Request({
|
||||
type: "plugin_name:simple",
|
||||
links: ["master-slave", "slave-instance"],
|
||||
forwardTo: "instance",
|
||||
requestProperties: {
|
||||
"instance_id": { type: "number" },
|
||||
"string_value": { type: "string" },
|
||||
},
|
||||
responseProperties: {
|
||||
"number_value": { type: "number" },
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
**Alpha 14 (JS)**
|
||||
```js
|
||||
const lib = require("@clusterio/lib");
|
||||
|
||||
class ForwardedEvent {
|
||||
static type = "event";
|
||||
static src = "instance";
|
||||
static dst = "controller";
|
||||
static plugin = "plugin_name";
|
||||
|
||||
constructor(stringValue, numberValue) {
|
||||
this.stringValue = stringValue;
|
||||
this.numberValue = numberValue;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["stringValue", "numberValue"],
|
||||
properties: {
|
||||
"stringValue": { type: "string" },
|
||||
"numberValue": { type: "number" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.stringValue, json.numberValue);
|
||||
}
|
||||
}
|
||||
|
||||
class SimpleRequest {
|
||||
static type = "request";
|
||||
static src = "controller";
|
||||
static dst = "instance";
|
||||
static plugin = "plugin_name";
|
||||
|
||||
constructor(stringValue) {
|
||||
this.stringValue = stringValue;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["stringValue"],
|
||||
properties: {
|
||||
"stringValue": { type: "string" },
|
||||
},
|
||||
};
|
||||
|
||||
static Response = lib.JsonNumber
|
||||
}
|
||||
|
||||
const plugin = {
|
||||
name: "plugin_name",
|
||||
messages: [
|
||||
ForwardedEvent,
|
||||
SimpleRequest,
|
||||
],
|
||||
...
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
plugin,
|
||||
ForwardedEvent,
|
||||
SimpleRequest,
|
||||
}
|
||||
```
|
||||
|
||||
**Alpha 14 (TS)**
|
||||
```ts
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { Static, Type } from "@sinclair/typebox";
|
||||
|
||||
export class ForwardedEvent {
|
||||
declare ["constructor"]: typeof ForwardedEvent;
|
||||
static type = "event" as const;
|
||||
static src = "instance" as const;
|
||||
static dst = "controller" as const;
|
||||
static plugin = "plugin_name" as const;
|
||||
|
||||
constructor(
|
||||
public stringValue: string,
|
||||
public numberValue: number,
|
||||
) { }
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
"stringValue": Type.String(),
|
||||
"numberValue": Type.Number(),
|
||||
});
|
||||
|
||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||
return new this(json.stringValue, json.numberValue);
|
||||
}
|
||||
}
|
||||
|
||||
export class SimpleRequest {
|
||||
declare ["constructor"]: typeof SimpleRequest;
|
||||
static type = "request" as const;
|
||||
static src = "controller" as const;
|
||||
static dst = "instance" as const;
|
||||
static plugin = "plugin_name" as const;
|
||||
|
||||
constructor(
|
||||
public stringValue: string,
|
||||
) { }
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
"stringValue": Type.String(),
|
||||
});
|
||||
|
||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||
return new this(json.stringValue);
|
||||
}
|
||||
|
||||
static Response = lib.JsonNumber
|
||||
}
|
||||
|
||||
export const plugin: lib.PluginDeclaration = {
|
||||
name: "plugin_name",
|
||||
messages: [
|
||||
ForwardedEvent,
|
||||
SimpleRequest,
|
||||
],
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Refactor message sending
|
||||
|
||||
Message handlers are now explicitly registered instead of relying on magic name lookups that don't make sense unless you know them.
|
||||
|
||||
**Alpha 13**
|
||||
```js
|
||||
const lib = require("@clusterio/lib");
|
||||
|
||||
class ControllerPlugin extends lib.BaseControllerPlugin {
|
||||
// event name is magically created by concatenating EventHandler or
|
||||
// RequestHandler to the name in the messages export in in info.js.
|
||||
async forwardedEventHandler(message) {
|
||||
// message.data contains the raw JSON data sent.
|
||||
if (message.data.string_value) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Alpha 14 (JS)**
|
||||
```js
|
||||
const { BaseControllerPlugin } = require("@clusterio/lib");
|
||||
const { ForwardedEvent } = require("./index.js");
|
||||
|
||||
class ControllerPlugin extends BaseControllerPlugin {
|
||||
async init() {
|
||||
// Explicit binding between message class and the handler for it
|
||||
this.controller.handle(ForwardedEvent, this.handleForwardedEvent.bind(this));
|
||||
// Note: On an instance plugin you would use this.instance.handle
|
||||
}
|
||||
|
||||
async handleForwardedEvent(event, src, dst) {
|
||||
// Event is an instance of the ForwardedEvent class
|
||||
if (event.stringValue) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// src and dst are instances of lib.Address and describe where
|
||||
// the message came from and who it was addressed to.
|
||||
}
|
||||
|
||||
...
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ControllerPlugin,
|
||||
}
|
||||
```
|
||||
|
||||
**Alpha 14 (TS)**
|
||||
```ts
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { BaseControllerPlugin } from "@clusterio/controller";
|
||||
import { ForwardedEvent } from "./index.js";
|
||||
|
||||
export class ControllerPlugin extends BaseControllerPlugin {
|
||||
async init() {
|
||||
// When using TypeScript this call is type checked to make sure the handler accepts the right class.
|
||||
this.controller.handle(ForwardedEvent, this.handleForwardedEvent.bind(this));
|
||||
// Note: On an instance plugin you would use this.instance.handle
|
||||
}
|
||||
|
||||
async handleForwardedEvent(event: ForwardedEvent, src: lib.Address, dst: lib.Address) {
|
||||
// Event is an instance of the ForwardedEvent class
|
||||
if (event.stringValue) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// src and dst describe where the message came from and who it was addressed to.
|
||||
}
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Refactor message sending
|
||||
|
||||
Message are now addressed end to end instead of using the confusing forwardTo and broadcastTo properties of the message definition.
|
||||
When broadcasting events the special `"allInstances"`, `"allHosts"`, and `"allControls"` shorthand addresses can be used.
|
||||
There's two methods for sending messages `.send` and `.sendTo` where `.send` is a shorthand for `.sendTo` with an address filled in as the other side of the link, this is mostly only useful in control plugins where the other side of the link is the controller.
|
||||
|
||||
**Alpha 13**
|
||||
```js
|
||||
// from instance plugin
|
||||
this.info.messages.forwarded.send(this.instance, {
|
||||
string_value: "foo",
|
||||
number_value: 123,
|
||||
});
|
||||
|
||||
// from master plugin
|
||||
const instance_id = ...
|
||||
const response = await this.forwardRequestToInstance(this.info.messages.simple, {
|
||||
instance_id,
|
||||
string_value: "foo",
|
||||
});
|
||||
const value = response.number_value;
|
||||
```
|
||||
|
||||
**Alpha 14**
|
||||
```js
|
||||
const { ForwardedEvent } = require("./info.js");
|
||||
|
||||
// from instance plugin
|
||||
this.instance.sendTo("controller", new ForwardedEvent("foo", 123));
|
||||
|
||||
// from controller plugin
|
||||
const instanceId = ...
|
||||
const value = await this.controller.sendTo({ instanceId }, new SimpleRequest("foo"));
|
||||
// if using TypeScript then the type of value will be inferred as number here
|
||||
```
|
||||
@@ -1,263 +0,0 @@
|
||||
# Protocol
|
||||
|
||||
Clusterio communicates using a simple homebrew message passing protocol based on JSON over WebSocket with limited support for reliable transport of messages.
|
||||
The protocol is implemented in [packages/lib/src/link](/packages/lib/src/link) and uses the `ws` library to handle the WebSocket connections on the Node.js side and the native WebSocket API on the browser side.
|
||||
|
||||
Two general kinds of messages are supported: events and requests.
|
||||
Events are messages sent by one party and received by the other and offer no notification for the sender on whether it was actually received.
|
||||
Requests are pairs of request-response messages where one side sends the requests and the other side replies with a response.
|
||||
Apart from the handshake used to establish connections the protocol is directionless, and either party can send or receive both requests and events.
|
||||
|
||||
Events and requests are defined as instances of the `Request` and `Event` classes, and specify a unique type string, who can send and receive the message, optional forwarding of the message and the format of the data payload as a JSON schema.
|
||||
All messages sent and received are validated with JSON schemas to catch potential errors in sending messages and avoid having to write lots of validation code in message handlers.
|
||||
|
||||
Messages are sent by calling the `.send()` method of the message instance and passing a `Link` instance to send it over and the data to send.
|
||||
For the controller there's a `Link` instance for each host and control connection stored in the `WsServer` class.
|
||||
For hosts the `Host` instance is a link to the controller, and each `Instance` instance is an in-memory link to the `Host` instance and has a corresponding `InstanceConnection` link stored on the `Host` instance.
|
||||
For ctl and web_ui there's a `Control` instance that acts as the link to the controller.
|
||||
|
||||
Messages received invokes message handlers that were registered when the link was brought up.
|
||||
These handlers are named after the name of the property the message is stored under with `EventHandler` appended for events and `RequestHandler` appended for requests.
|
||||
The handlers are defined on the link class itself for core messages, and on the plugin class in the case of plugin defined messages.
|
||||
|
||||
|
||||
## Connection flow
|
||||
|
||||
Clients connect via WebSocket to the `/api/socket` endpoint and upon establishing connection to the server the connection is in the handshake phase and the server will send a `hello` message to the client.
|
||||
If the client has no previous session with the server it sends a `register_host` or `register_control` message depending on what kind of client it is to the server.
|
||||
If the register is valid then the server will respond with a `ready` message with a session token and the connection has left the handshake phase, otherwise the server will close the connection with a WebSocket close code indicating the kind of failure.
|
||||
|
||||
Upon leaving the handshake phase both parties should start sending heartbeat messages with the sequence number of the last received message.
|
||||
Messages sent over the link should be stored in a send buffer and only cleared from the send buffer when a heartbeat with a `last_seq` equal or or higher is received or the session is invalidated.
|
||||
|
||||
If the connection goes stale or is unexpectedly closed the client can reastablish the same session by connecting to the server again and sending a `resume` message after received the `hello` from the server.
|
||||
If the session resume is successful then the server will respond with a `continue` message and the connection is active again.
|
||||
The client should then re-send all messages it has in its send buffer that is greater than the `last_seq` number in the `continue` message.
|
||||
Otherwise the server will respond with `invalidate` and the client is expected to drop its send buffer start a new session with `register_host` or `register_control`.
|
||||
|
||||

|
||||
|
||||
|
||||
## Connector flow
|
||||
|
||||
To manage the complexity of connecting, reconnecting, resuming or invalidating dropped sessions the WebSocket connection for a `Link` class is managed by a connector.
|
||||
There's three kinds of connectors defined: `WebSocketClientConnector`, `WebSocketServerConnector` and `VirtualConnector`.
|
||||
The virtual connector is for in-memory connections and doesn't have any connection state logic.
|
||||
|
||||
For the WebSocket connectors there's four states they can exist in:
|
||||
- `closed` - The connector has just been created or has been explicitly closed.
|
||||
- `connecting` (client only) - The connector is in the process of trying to connect to the server.
|
||||
- `connected` - The connector is successfully connected.
|
||||
- `resuming` - The connection dropped and the connector is in the process of attempting to resume it.
|
||||
|
||||
Along with the following events emitted when transition between states:
|
||||
- `connect` From `connecting` (client) or `closed` (server) to `connected`
|
||||
- `drop` - From `connected` to `resuming`
|
||||
- `resume` - From `resuming` to `connected`
|
||||
- `invalidate` - From `resuming` to `connecting` or `closed`
|
||||
- `close` - From any state to `closed`
|
||||
|
||||

|
||||
|
||||
|
||||
### WsClientConnector
|
||||
|
||||
The client connector will keep trying to establish and mantain a connection after `.connect()` is called on it, stopping only in the case of authentication failure or `.close()`/`.setClosing()` being called.
|
||||
Other than that, the logic is pretty straight forward.
|
||||
|
||||

|
||||
|
||||
|
||||
### WsServerConnector
|
||||
|
||||
The server connector doesn't do the work of connecting or validating the handshake.
|
||||
Instead it's handed over a WebSocket connection from `WsServer` once there's only the step of sending the final message left to do.
|
||||
Because of this there's no `connecting` state, and once a session times out from the `resuming` state it goes directly to closed.
|
||||
|
||||

|
||||
|
||||
|
||||
## Wire format
|
||||
|
||||
All communication in the protocol is done via WebSocket text frames containing a JSON payload with the following properties:
|
||||
|
||||
- `seq: integer | null` - The sequence number of the message, can only be null for handshake and heartbeats.
|
||||
- `type: string` - The type of message
|
||||
- `data: object` - Message data
|
||||
|
||||
Before a connection is established it's in the handshake phase and the `seq` property is `null`.
|
||||
The `type` property is used to distinguish which of handshake, heartbeat, events or requests this message is and determines the expected format of the `data` property.
|
||||
|
||||
|
||||
### Handshake Messages
|
||||
|
||||
The following messages are the only valid messages that can be sent during the handshake phase.
|
||||
|
||||
|
||||
#### `hello`
|
||||
|
||||
Sent by the controller when the WebSocket connection has been opened.
|
||||
|
||||
- `version: string` - The version of the controller, e.g. "2.0.0".
|
||||
- `plugins: Object<string, string>` -
|
||||
Object mapping plugin names to plugin version for plugins that are loaded on the controller.
|
||||
|
||||
#### `register_host`
|
||||
|
||||
Host handshake for establishing a new connection session.
|
||||
|
||||
- `token: string` - Authentication token.
|
||||
- `version: string` - The protocol version of the host, e.g. "2.0.0".
|
||||
- `id: integer` - ID of the host.
|
||||
- `plugins: Object<string, string>` -
|
||||
Object mapping plugin names to plugin version for plugins that are available on the host.
|
||||
|
||||
#### `register_control`
|
||||
|
||||
Control handshake for enstablishing a new connection session.
|
||||
|
||||
- `token: string` - Authentication token.
|
||||
- `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 controller 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 controller to signal resuming the session was successful.
|
||||
|
||||
- `last_seq: integer | null` - Last message seq received by the server.
|
||||
- `session_timeout: integer` - Session timeout in seconds.
|
||||
- `heartbeat_interval: number` - Rate client should send heartbeats at.
|
||||
|
||||
#### `ready`
|
||||
|
||||
Sent by the controller to signal the connection has been established with a new session.
|
||||
|
||||
- `session_token: string` - Token to resume the session with.
|
||||
- `session_timeout: integer` - Session timeout in seconds.
|
||||
- `heartbeat_interval: number` - Rate client should send heartbeats at.
|
||||
|
||||
|
||||
### Heartbeat message
|
||||
|
||||
Once connected as indicated by the `ready` and `continue` messages both sides of the connection starts sending heartbeats at the inverval given in the `ready` and `continue` messages.
|
||||
The heartbeat should contain the `seq` of the last received message, and receiving a heartbeat with a given sequence number means that the other party of the link has received all messages up to that seq number.
|
||||
|
||||
|
||||
#### `heartbeat`
|
||||
|
||||
Sent by both the controller and clients at the interval given in the continue/ready messages.
|
||||
|
||||
- `last_seq: integer` - Last message seq received over the link.
|
||||
|
||||
### 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/src/link/messages.ts](/packages/lib/src/link/messages.ts) for the recognized core events and their contents.
|
||||
Plugins may defined their own events as well.
|
||||
|
||||
#### `*_event`
|
||||
|
||||
- any properties defined by the event type.
|
||||
|
||||
|
||||
### 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/src/link/messages.ts](/packages/lib/src/link/messages.ts) for the recognized core requests and their contents.
|
||||
Plugins may define their own requests as well.
|
||||
|
||||
#### `*_request`
|
||||
|
||||
- any properties defined by the request type.
|
||||
|
||||
#### `*_response` success
|
||||
|
||||
- `seq: integer` - The seq number of the request message.
|
||||
- any additional properties defined by the request type.
|
||||
|
||||
#### `*_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.
|
||||
|
||||
|
||||
### Example handshake
|
||||
|
||||
Server hello after opening the WebSocket.
|
||||
|
||||
```json
|
||||
{
|
||||
"seq": null,
|
||||
"type": "hello",
|
||||
"data": {
|
||||
"version": "2.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Host registering a new connection session.
|
||||
|
||||
```json
|
||||
{
|
||||
"seq": null,
|
||||
"type": "register_host",
|
||||
"data": {
|
||||
"token": "xyzabc123",
|
||||
"version": "2.0.0",
|
||||
"id": 123
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Controller reply signaling the session is established and ready.
|
||||
|
||||
```json
|
||||
{
|
||||
"seq": null,
|
||||
"type": "ready",
|
||||
"data": {
|
||||
"session_token": "xyz.azy.bzy",
|
||||
"session_timeout": 60,
|
||||
"heartbeat_interval": 15
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Host and controller can now send events and requests over the connection.
|
||||
@@ -1,16 +0,0 @@
|
||||
## 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 controller 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 a Cluster Admin role to get access to manage it.
|
||||
@@ -1,263 +0,0 @@
|
||||
# Managing a Cluster
|
||||
|
||||
Clusterio clusters are managed through the controller 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 clusteriocontroller bootstrap create-ctl-config <username>` on the controller, which creates the necessary `config-control.json` for managing the cluster as the given user.
|
||||
|
||||
|
||||
## Hosts
|
||||
|
||||
### List hosts
|
||||
|
||||
ctl> host list
|
||||
|
||||
Lists all hosts.
|
||||
|
||||
### Generate token
|
||||
|
||||
ctl> host generate-token [--id <host id>]
|
||||
|
||||
Generate a token for a host. `--id` is the host id.
|
||||
|
||||
### Create config
|
||||
|
||||
ctl> host create-config [--id <host id>] [--name <host name>] [--generate-token <bool>] [--output <path to output>]
|
||||
|
||||
Create a host config. `--id` id the host id. `--name` is the host name. `--generate-token` decides whether to also generate a token, defaulting to `0`. `--output` is the filename for the output of the config; Default is "config-host.json", "-" for stdout.
|
||||
|
||||
### Update Clusterio
|
||||
|
||||
ctl> host update [--id <host id>] [--name <host name>] [--restart]
|
||||
|
||||
If remote updates are enabled on the host, this will attempt to update `@clusterio/host` on the machine. Changes will not be applied until the host is restarted. If the `--restart` flag is provided, the host will automatically restart once the update is complete.
|
||||
|
||||
Remote updates are enabled by default but can be disabled on a per-machine basis at the owner's discretion. For security reasons, this configuration setting cannot be changed remotely. To modify it, run the following command locally while the host is offline:
|
||||
|
||||
host> config set host.allow_remote_updates true/false
|
||||
|
||||
The controller also supports remote updates, following the same logic. The corresponding commands are:
|
||||
|
||||
ctl> controller update [--restart]
|
||||
controller> config set controller.allow_remote_updates true/false
|
||||
|
||||
## Instances
|
||||
|
||||
### List Instances
|
||||
|
||||
ctl> instance list
|
||||
|
||||
List instances.
|
||||
|
||||
### Create instance
|
||||
|
||||
ctl> instance create <name> [--id <instance id>]
|
||||
|
||||
Create a new instance. `name` is the instance name. `--id` is the optional id.
|
||||
|
||||
### Kill instance
|
||||
|
||||
ctl> instance kill <name>
|
||||
|
||||
Terminates the running Factorio server without giving it a chance to save or cleanup after itself.
|
||||
|
||||
Note: This may cause loss of data.
|
||||
|
||||
### Extract players from instance
|
||||
|
||||
ctl> instance extract-players <name>
|
||||
|
||||
Creates a user account in the cluster for each player that has been online on the currently running save of the instance and sets the online time of that account on this instance to the online time recorded in the save.
|
||||
|
||||
Useful when importing a save to a cluster.
|
||||
Note that the online time in a save is recorded differntly from how Clusterio records online time.
|
||||
Most notably Clusterio records wall clock time while the save time is recorded in ticks and this extraction of player time assumes the game runs at 60 ticks a second.
|
||||
|
||||
|
||||
### Config management
|
||||
|
||||
#### List Configuration
|
||||
|
||||
ctl> instance config list <instance>
|
||||
|
||||
List config values for an instance. `instance` is the instance whose config values to list.
|
||||
|
||||
#### Set config value
|
||||
|
||||
ctl> instance config set <instance> <field> [value] [--stdin]
|
||||
|
||||
Set field in instance config. `instance` is instance to set. `field` is field to set. `value` is value to set in field. `--stdin` signals to read value from stdin.
|
||||
|
||||
#### Set config property of field
|
||||
|
||||
To be written.
|
||||
|
||||
#### Edit instance config
|
||||
|
||||
ctl> instance config edit <instance> [editor]
|
||||
|
||||
Edit instance config in a text editor. Once file is saved and editor has been exited, config values will be read back in. `instance` is the instance to edit, `editor` is the editor to use. If `editor` is not given, clusterioctl will read from the `EDITOR` or `VISUAL` enviroment variables.
|
||||
|
||||
### Assign instance to host
|
||||
|
||||
ctl> instace assign <instance> [host]
|
||||
|
||||
Assigns instance to host. `instance` is instance to assign, `host` is host to assign to. If `host` is not provided, instance will be unassigned.
|
||||
|
||||
## 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.
|
||||
|
||||
### Bulk importing admin, whitelist and ban lists
|
||||
|
||||
Whitelists and admin lists can be bulk imported with the following commands:
|
||||
|
||||
ctl> user whitelist-bulk-import <file> [--create]
|
||||
ctl> user admin-bulk-import <file> [--create]
|
||||
|
||||
The file should simply be newline seperated names:
|
||||
|
||||
joe
|
||||
bob
|
||||
allan
|
||||
|
||||
The `--create` option will create the user if they do not exist, and then set the relevent value.
|
||||
|
||||
Banlists can be bulk imported with the following command:
|
||||
|
||||
ctl> user banned-bulk-import <file> [--create]
|
||||
|
||||
The file should be names, followed by a ban reason:
|
||||
|
||||
joe greifed copper mines
|
||||
bob stole materials
|
||||
allan rude language
|
||||
|
||||
The first string on the line is used as the username, the others are used as the ban reason.
|
||||
The `--create` option will create the user if they do not exist, then ban them.
|
||||
|
||||
### 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 Cluster 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 controller 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.
|
||||
|
||||
|
||||
## Plugins (Local)
|
||||
|
||||
To be written.
|
||||
|
||||
|
||||
## Plugins (Remote)
|
||||
|
||||
By default, remote updates are enabled unless you selected "no" during installation.
|
||||
Remote updates allow ctl and the web UI to update plugins on machines within the cluster.
|
||||
However, remote installations are disabled by default to prevent the risk of arbitrary code execution through the installation of malicious npm packages onto the machine. See below if you wish to enable this feature.
|
||||
|
||||
### List
|
||||
|
||||
ctl> controller plugin list
|
||||
ctl> host plugin list <host>
|
||||
|
||||
This command lists all plugins currently known to the remote. Newly installed plugins will not be displayed until the remote is restarted. Plugins that are distributed as an npm package can be updated remotely.
|
||||
|
||||
### Update
|
||||
|
||||
ctl> controller plugin update <name> [--restart]
|
||||
ctl> host plugin update <host> <name> [--restart]
|
||||
|
||||
If remote plugin updates are enabled on the remote, this will execute `npm update --save <name>`. Changes will not be applied until the remote is restarted. If the `--restart` flag is provided, the remote will automatically restart once the update is complete.
|
||||
|
||||
### Install
|
||||
|
||||
ctl> controller plugin install <name> [--restart]
|
||||
ctl> host plugin install <host> <name> [--restart]
|
||||
|
||||
If remote plugin installs are enabled on the remote, this will execute `npm install --save <name>`. Changes will not be applied until the remote is restarted. If the `--restart` flag is provided, the remote will automatically restart once the update is complete.
|
||||
|
||||
### Enable/Disable Updates
|
||||
|
||||
host> config set host.allow_plugin_updates true/false
|
||||
controller> config set controller.allow_plugin_updates true/false
|
||||
|
||||
Remote updates are enabled by default but can be disabled on a per-machine basis at the owner's discretion. For security reasons, this configuration setting cannot be changed remotely. To modify it, run the above commands locally while the remote is offline.
|
||||
|
||||
### Enable/Disable Installs
|
||||
|
||||
host> config set host.allow_plugin_install true/false
|
||||
controller> config set controller.allow_plugin_install true/false
|
||||
|
||||
Remote installs are disabled by default but can be enabled on a per-machine basis at the owner's discretion. For security reasons, this configuration setting cannot be changed remotely. To modify it, run the above commands locally while the remote is offline.
|
||||
@@ -1,46 +0,0 @@
|
||||
# Migrating from 2.0.0-alpha.13 to 2.0.0-alpha.14
|
||||
|
||||
Alpha 14 comes with significant breaking changes for existing clusters, most notably the rename of master and slave to controller and host.
|
||||
Please study the entire migration guide carefully before attempting to update an existing cluster.
|
||||
|
||||
## Before updating
|
||||
|
||||
If you're using mods you need to create mod pack string for the mod configurations you're currently running.
|
||||
You can do this by running the follow RCON command on an instance that is currently running the mod configuration you want to use (you can also run this command in single player and find the string printed in Factorio's [Log file](https://wiki.factorio.com/Log_file)):
|
||||
|
||||
/sc local m={name="pack",description="",factorio_version=game.active_mods.base,mods={},settings={startup={},["runtime-global"]={},["runtime-per-user"]={}}} for n,v in pairs(game.active_mods) do table.insert(m.mods,{name=n,enabled=true,version=v}) end local function set(s,c) for n,v in pairs(s) do m.settings[c][n]=v end end set(settings.startup,"startup") set(settings.global,"runtime-global") if game.player then set(game.player.mod_settings,"runtime-per-user") else set(settings.player,"runtime-per-user") end local e=game.encode_string(game.table_to_json(m)) log(e) rcon.print(e)
|
||||
|
||||
If you for some reason want to run this function in 2.0+, use the following command:
|
||||
|
||||
/sc local a={name="pack",description="",factorio_version=script.active_mods.base,mods={},settings={startup={},["runtime-global"]={},["runtime-per-user"]={}}}for b,c in pairs(script.active_mods)do table.insert(a.mods,{name=b,enabled=true,version=c})end;local function d(e,f)for b,c in pairs(e)do a.settings[f][b]=c end end;d(settings.startup,"startup")d(settings.global,"runtime-global")if game.player then d(game.player.mod_settings,"runtime-per-user")else d(settings.player,"runtime-per-user")end;local g=helpers.encode_string(helpers.table_to_json(a))log(g)rcon.print(g)
|
||||
|
||||
This should produce a [Mod Pack String](https://forums.factorio.com/viewtopic.php?f=96&t=103578) which you need to copy and paste somewhere to keep it for later.
|
||||
Mod Pack strings looks something like this:
|
||||
|
||||
eNqFketuwyAMhd/Fv0OUVpW25lWmqSLESdAIRNh0l4h3n9mlitZt5QdCx/7so8MKXs8ILSzaPEEFPZKJdmEbvIgiDNpwiDaczhjpU93Vu/r+Tmpz6Anah/V7RqcJRUavO4c9tBwTVnAF5upCGJeIsYx3tvsPbQTdb0lKHYlnPJHY0+ONvcdjvW8gP1ZAyGz9KL5XINaR0yLPXEFMnu2ManSh0+6j/GOFitqXG4mjNSUidVm5wlm7JMYG7Qhl3BX8FjyqZ9vztOk+NM2fvRPaceLbzbN+UejQFFOWXzfArvk6v2HWD9ZLv5JfxA1UwsubPBaMKhHGklLO76vAwfQ=
|
||||
|
||||
If you have multiple mod configuration create a mod pack string for each and note them down.
|
||||
|
||||
Double check that you are on a supported version of Node.js by running `node --version`, the version should be v18 or later, if not you will need to update Node.js before continuing.
|
||||
|
||||
|
||||
## Migrate installations
|
||||
|
||||
**Note:** Check that plugins you are using have updates avaialble for alpha 14 before attempting to update, if they do not either wait for the plugin to be updated or uninstall the plugin from all places it is installed on first.
|
||||
|
||||
Shutdown all slaves and the master server and then migrate each installations by running the following command:
|
||||
|
||||
npm exec @clusterio/create -y -- --migrate-rename --log-level=verbose
|
||||
|
||||
This will convert the existing installation's usage of the master and slave names in packages, configs, database and logs to the new controller and host terms and update the installed packages.
|
||||
|
||||
If you're using systemd then copy the generated clusteriocontroller.service and/or clusteriohost.service files from the systemd folder to /etc/systemd/system/ and remove the old clusteriomaster.service and clusterioslave.service files.
|
||||
|
||||
Once the installations have been migrated start up the controller and hosts again.
|
||||
|
||||
|
||||
## Post installation
|
||||
|
||||
Import the Mod Pack strings that was created beforehand on the controller and upload the mods needed by those mod packs to the controller.
|
||||
You can assign a default mod pack in the controller config which instances use by default or set the mod pack on the per instance basis in the instance config.
|
||||
Once you have assigned a mod pack to an instance run a data export on the instance in order to properly populate the mod settings in the mod and get item icons and item names resolved in the web interface.
|
||||
@@ -1,67 +0,0 @@
|
||||
# Release workflow
|
||||
|
||||
## Update changelog
|
||||
|
||||
- CHANGELOG.md in the repository root
|
||||
- The changelog.txt file in mod directories
|
||||
|
||||
Pull requests will usually have suggestions for changelog entries.
|
||||
|
||||
## Packages
|
||||
|
||||
> [!NOTE]
|
||||
> You will need commit access to the repository as well publish access to the @clusterio org on npm to do this.
|
||||
|
||||
1. Ensure you are on the latest version of master branch with no untracked files
|
||||
|
||||
```sh
|
||||
git switch master
|
||||
git pull --ff-only upstream master
|
||||
git status
|
||||
```
|
||||
|
||||
2. Bump the version in the `package.json` files
|
||||
|
||||
```sh
|
||||
sed -i 's/2\.0\.0-alpha\.XX/2\.0\.0-alpha\.YY/' {packages,plugins}/*/package.json
|
||||
````
|
||||
|
||||
3. Tag and push the release to github
|
||||
|
||||
```sh
|
||||
git add -p
|
||||
git commit -m v2.0.0-alpha.XX
|
||||
git tag v2.0.0-alpha.XX
|
||||
```
|
||||
|
||||
4. Perform a clean install and publish the packages.
|
||||
|
||||
```sh
|
||||
pnpm clean
|
||||
pnpm -r --filter='./{plugins,packages}/*' publish
|
||||
```
|
||||
|
||||
## Plugins
|
||||
|
||||
1. Bump the version in the `package.json` file
|
||||
2. Run `pnpm install` in the repository root
|
||||
3. Run `pnpm adduser` to authenticate
|
||||
4. Run `pnpm publish` in the plugin/package directory
|
||||
|
||||
Before confirming the publish, ensure the packed size is approximately the same size as the last version (or within the expected size)
|
||||
|
||||
After publishing, test creating and installing the plugin on a fresh install.
|
||||
|
||||
## Lua mods
|
||||
|
||||
To publish lua mods like clusterio_lib, start by building from source:
|
||||
|
||||
```sh
|
||||
pnpm run build-mod
|
||||
```
|
||||
|
||||
or download the latest artifacts from the CI. When uploading the mods to the mod portal, make sure the latest version is the last version uploaded, as the game/mod portal/ingame updater isn't consistent in how it figures out the "latest version"
|
||||
|
||||
## Release notes
|
||||
|
||||
Release notes are expected to be published alongside new releases. We use the github releases system as well as posting in the #announcements channel on the discord. Major releases may warrant a ping.
|
||||
@@ -1,72 +0,0 @@
|
||||
# Setting up TLS
|
||||
|
||||
By default Clusterio will listen for connections over HTTP which is fine for local networks but over the Internet it should idealy be over HTTPS as there are some credentials that could be exposed to eavesdroppers.
|
||||
The recommended setup is to have an ordinary HTTPS server software like Apache or Nginx to act as a reverse proxy in front of the Clusterio controller.
|
||||
The reverse proxy would then be set up with a TLS certificate from a Certificate Authoritiy like [Let's Encrypt](https://letsencrypt.org).
|
||||
Alternatively you can have Clusterio listen for connections over HTTPS directly by providing it with a TLS certificate and private key.
|
||||
|
||||
Note that a TLS certificate signed by a recognized Certificate Authority in general requires that you own a domain.
|
||||
See the last section for how to create and use a self-signed certificate for an IP address if you don't have a domain.
|
||||
|
||||
|
||||
## Using a reverse proxy
|
||||
|
||||
Configure the proxy to forward requests over HTTP to the port the controller is listening to so that for example a request to `https://example.com/instances/` is forwarded to `http://localhost:8080/instances/`.
|
||||
It is possible to host the interface under a sub-path, in that case a request under the sub-path should be mapped to the controller with the sub-path stripped out.
|
||||
For example `https://example.com/sub/path/instances/` would be mapped to `http://localhost:8080/instances`.
|
||||
|
||||
Additianlly WebSocket connections to `/api/socket` also needs to be forwarded.
|
||||
Typically this requires explicit configuration and support for forwarding WebSocket connections.
|
||||
If the interface hosted under a sub-path the `/api/socket` address is relative to the sub-path and the sub-path also needs to be stripped out when forwarded.
|
||||
|
||||
You can also optionally set the controller to listen only on the loopback interface to prevent it being directly connected to from the outside.
|
||||
|
||||
npx clusteriocontroller config set controller.bind_address 127.0.0.1
|
||||
|
||||
|
||||
### Apache Config
|
||||
|
||||
To proxy with Apache make sure that `proxy_module`, `proxy_http_module` and `proxy_wstunnel_module` is loaded in the config, then add the following directives to wherever is the approriate place for your flavour of Apache config organization.
|
||||
|
||||
```apache
|
||||
<Location /sub/path/>
|
||||
ProxyPass "http://localhost:8080/"
|
||||
</location>
|
||||
<Location /sub/path/api/socket>
|
||||
ProxyPass "ws://localhost:8080/api/socket"
|
||||
</Location>
|
||||
```
|
||||
|
||||
Replace `/sub/path/` with the location on the proxy you want to host the Clusterio interface under.
|
||||
If the whole domain is to be dedicated to the Clusterio interface then `/sub/path/` should be just `/`.
|
||||
Pay attention to the use of trailing slashes, as they are important.
|
||||
|
||||
|
||||
## Hosting HTTPS directly from Node.js
|
||||
|
||||
Listening on HTTPS is supported by the Clusterio controller via the `controller.https_port` option provided paths to a certificate file and a private key file is configured for the `controller.tls_certificate` and `controller.tls_private_key` options.
|
||||
Both of these are expected to be unencrypted PEM files.
|
||||
|
||||
|
||||
## Creating and using a self-signed certificate
|
||||
|
||||
Create a self-signed certificate with OpenSSL by running
|
||||
|
||||
openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 3650 -subj /CN=localhost -addext "subjectAltName=IP:<ip>"
|
||||
|
||||
substituting `<ip>` with the IP address the server will be accessed under, for example `IP:203.0.113.4`.
|
||||
Multiple addresses can be given by supplying a comma sepparated list of `IP:address` pairs and DNS names can also be specified using the `DNS:example.com` format.
|
||||
You need to supply every name you want to connect to the server under the subjectAltName field, for example to connect to the HTTPS port locally via an address like `https://localhost:4443/` you need to add `DNS:localhost`.
|
||||
|
||||
This creates a private key file named `key.pem` and a public certificate file named `cert.pem` and these are used for the `controller.tls_certificate` and `controller.tls_private_key` config options respectively on the controller:
|
||||
|
||||
npx clusteriocontroller config set controller.tls_certificate cert.pem
|
||||
npx clusteriocontroller config set controller.tls_private_key key.pem
|
||||
|
||||
Additionally you will need copy the `cert.pem` file to all of the computers you want to set up as hosts and configure it as the `host.tls_ca` option:
|
||||
|
||||
npx clusteriohost config set host.tls_ca cert.pem
|
||||
|
||||
You will also need to copy the `cert.pem` file to all of the computers you want to remotely manage the cluster via clusterctl and configure it as `control.tls_ca` option:
|
||||
|
||||
npx clusterioctl control-config set control.tls_ca cert.pem
|
||||
@@ -1,595 +0,0 @@
|
||||
# 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)
|
||||
- [Logging Messages](#logging-messages)
|
||||
- [Plugin Configuration](#plugin-configuration)
|
||||
- [Communicating with Factorio](#communicating-with-factorio)
|
||||
- [Defining Link Messages](#defining-link-messages)
|
||||
- [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/
|
||||
+- index.js
|
||||
+- package.json
|
||||
+- controller.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`.
|
||||
After creating a directory for your plugin, the boilerplate can be generated automaticlly from a set of tempaltes using `npm exec @clusterio/create -y -- --plugin-template` after which you will be asked which areas your plugin will interact with and your plugin's name.
|
||||
To add it to `plugin-list.json` so that it gets loaded use the `plugin add <path>` sub-command to either clusteriocontroller, clusteriohost or clusterioctl.
|
||||
Note that it's important that the path starts with ./ or ../ (use .\ or ..\ on Windows).
|
||||
|
||||
For a plugin to be recognised by Clusterio it needs to export an entry named `plugin` from its main entrypoint.
|
||||
By default the main entrypoint is the `index.js` file, but this may be changed by setting the `"main"` entry to a different file in `package.json`.
|
||||
Here's an example of `index.js`:
|
||||
|
||||
```js
|
||||
module.exports.plugin = {
|
||||
name: "foo_frobber",
|
||||
title: "Foo Frobber",
|
||||
description: "Does advanced frobnication",
|
||||
instanceEntrypoint: "instance",
|
||||
controllerEntrypoint: "controller",
|
||||
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 controller in order for it to be possible to load it on an instance.
|
||||
|
||||
**instanceConfigFields**:
|
||||
Object mapping instance configuration fields to `FieldDefinitions`for this plugin.
|
||||
See [Plugin Configuration](#plugin-configuration)
|
||||
|
||||
**controllerEntrypoint**:
|
||||
Path to a Node.js module relative to the plugin directory which contains the ControllerPlugin class definiton for this plugin.
|
||||
This is an optional parameter.
|
||||
A plugin can be made that only runs on the controller.
|
||||
|
||||
**ControllerConfigGroup**:
|
||||
Object mapping controller configuration fields to `FieldDefinitions`for this plugin.
|
||||
See [Plugin Configuration](#plugin-configuration)
|
||||
|
||||
**ctlEntrypoint**:
|
||||
Path to a Node.js module relative to the plugin directory which contains the CtlPlugin 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 loaded.
|
||||
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 ControllerPlugin class definition into controller.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 controllerEntrypoint 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 ControllerPlugin class the following code can be used:
|
||||
|
||||
```js
|
||||
const { BaseControllerPlugin } = require("@clusterio/controller");
|
||||
|
||||
class ControllerPlugin extends BaseControllerPlugin {
|
||||
async init() {
|
||||
this.foo = 42;
|
||||
await this.startFrobnication();
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ControllerPlugin,
|
||||
}
|
||||
```
|
||||
|
||||
For the instance plugin it's exactly the same except "Controller" is replaced with "Instance", for host plugins "Host" is used and for the clusterioctl plugin "Ctl" is used.
|
||||
The available hooks that you can override are documented in the base classes.
|
||||
|
||||
It's best to avoid defining a constructor, but if you insist on defining one, forward all arguments to the base class.
|
||||
E.g.:
|
||||
|
||||
```js
|
||||
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.
|
||||
|
||||
|
||||
## Logging Messages
|
||||
|
||||
The base plugin classes provide a winston logger for logging messages to the shared cluster log.
|
||||
For instances a copy of the log is also stored on the host the instance is on.
|
||||
To use it, pass a string to one of the log levels functions, for example:
|
||||
|
||||
```js
|
||||
async init() {
|
||||
this.logger.info("Initializing frobbing");
|
||||
}
|
||||
```
|
||||
|
||||
Metadata covering which plugin and instance (for instance plugin classes) the log event happened on will automatically be attached to the log message.
|
||||
|
||||
The available levels are fatal, error, warn, audit, info, and verbose.
|
||||
These levels have roughly the following meanings:
|
||||
|
||||
- fatal: Unrecoverable error that prevents continuing execution of the Node.js process.
|
||||
Note that Plugins shouldn't have fatal errors.
|
||||
- error: An unexpected error condition occured, usually an exception thrown when it wasn't expected.
|
||||
Should be investigated and fixed.
|
||||
- warn: An potential error was detected, but operation continued gracefully.
|
||||
These may indicate that something is running sub-optimally, or is not working as intedend.
|
||||
- audit: Record of actions performed by users of the cluster.
|
||||
This is usually emitted in response to management requests by a control link.
|
||||
- info: A general log message.
|
||||
May indicae a status or operation completed.
|
||||
This should not be emitted by periodic tasks that run often.
|
||||
- verbose: A verbose log message which might be useful when investigating issues, does not show by default.
|
||||
Note that logging messages is not a cheap operation even when the messages do not show up and spammy logging should be avoided for the verbose level.
|
||||
If the logs are still valuable in certain cases consider putting them behind a config option.
|
||||
|
||||
For guarding unexpected errors the best option is to log a short description along with the`stack` property of the Error:
|
||||
|
||||
```js
|
||||
try {
|
||||
// Operation that should not throw but may end up throwing
|
||||
} catch (err) {
|
||||
this.logger.error(`Operation failed:\n${err.stack}`);
|
||||
)
|
||||
```
|
||||
|
||||
For plugin hooks expections thrown are automatically catched and logged, but for event handlers registered on EventEmitters it's critical that exceptions are catched and handled appropriately.
|
||||
|
||||
|
||||
## Plugin Configuration
|
||||
|
||||
Clusterio provides a configuration system that handles storing, distributing, editing and validating config fields for you.
|
||||
You can take advantage of it by declaring config fields under either `controllerConfigFields` or `instanceConfigFields` in the `plugin` export.
|
||||
For example in index.js:
|
||||
|
||||
```ts
|
||||
import type * as lib from "@clusterio/lib";
|
||||
|
||||
declare module "@clusterio/lib" {
|
||||
// Extend the interface defining available fields so that instance.config.get(...)
|
||||
// will have our own options.
|
||||
export interface ControllerConfigFields {
|
||||
"foo_frobber.level": number, // If optional is true then this should have | null in addition.
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
...
|
||||
controllerConfigFields: {
|
||||
"foo_frobber.level": {
|
||||
description: "Level of frobnication done",
|
||||
type: "number",
|
||||
initialValue: 2,
|
||||
},
|
||||
}
|
||||
} satisfies lib.PluginDeclaration;
|
||||
```
|
||||
|
||||
Code inside the `ControllerPlugin` class will then be able to access the level config field through the `Config` object at `this.controller.config`, for example in the ControllerPluginClass:
|
||||
|
||||
```ts
|
||||
async init() {
|
||||
let level = this.controller.config.get("foo_frobber.level");
|
||||
this.logger.info(`I got a frobnication level of ${level}`);
|
||||
}
|
||||
```
|
||||
|
||||
The same applies for instance configs, replace "controller" with "instance" where appropriate.
|
||||
See [Configuration System](config-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:
|
||||
|
||||
```js
|
||||
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 plugin object.
|
||||
For example:
|
||||
|
||||
```js
|
||||
async onStart() {
|
||||
let response = await this.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:
|
||||
|
||||
```js
|
||||
async init() {
|
||||
this.instance.server.on("ipc-my_plugin_foo", content =>
|
||||
this.handleFoo(content).catch(err => this.logger.error(
|
||||
`Error handling foo:\n${err.stack}`
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
async handleFoo(content) {
|
||||
// Do stuff with content
|
||||
}
|
||||
```
|
||||
|
||||
And then in the module for the plugin:
|
||||
|
||||
```lua
|
||||
local clusterio_api = require("modules/clusterio/api")
|
||||
|
||||
-- inside some event handler
|
||||
clusterio_api.send_json("my_plugin_foo", { data = 123 })
|
||||
```
|
||||
|
||||
For convience a helper function is exposed on server specifically for handling IPC events.
|
||||
|
||||
```js
|
||||
async init() {
|
||||
this.instance.server.handle("my_plugin_foo", this.handleFoo.bind(this));
|
||||
}
|
||||
|
||||
async handleFoo(content) {
|
||||
// Do stuff with content
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** This only handles IPC events, all other events must use `on`. You should not include the `ipc-` prefix when using this method. Error logging is handled for you, if you want custom error handling use `on`.
|
||||
|
||||
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 controller or other instances in your plugin for it to do anything useful.
|
||||
For this there's a WebSocket communication channel established between the hosts and the controller 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, except both parties of a link may initiate one.
|
||||
|
||||
Messages are defined as items of the `plugin.messages` array exported by `index.js`.
|
||||
For example, the following could be defined in `plugin`:
|
||||
|
||||
```js
|
||||
messages: [
|
||||
class Frobnicate {
|
||||
static plugin = "foo_frobber"; // Plugin name
|
||||
static type = "request";
|
||||
static src = "controller";
|
||||
static dst = "instance";
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
"verbosity": { type: "integer" },
|
||||
"special": { type: "boolean" },
|
||||
},
|
||||
required: ["verbosity"],
|
||||
additionalProperties: false,
|
||||
};
|
||||
constructor(json) {
|
||||
this.verbosity = json.verbosity;
|
||||
this.special = json.special;
|
||||
}
|
||||
static fromJSON(json) {
|
||||
return new Frobnicate(json);
|
||||
}
|
||||
toJSON() {
|
||||
return {
|
||||
verbosity: this.verbosity,
|
||||
special: this.special,
|
||||
};
|
||||
}
|
||||
static Response = {
|
||||
jsonSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
"report": {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
fromJSON(json) {
|
||||
return json
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
This specifies a request that can be sent from the controller to an instance, being routed through the correct host behind the scenes.
|
||||
The request data must contain the property `verbosity` with an integer number as the value, and it may also contain a boolean `special` property.
|
||||
It also defines that the response sent may contain a `report` property mapping to an array of strings.
|
||||
|
||||
The following properties are recognized as part of a message class:
|
||||
|
||||
#### static plugin
|
||||
|
||||
Must contain the name of the plugin this message is defined by.
|
||||
This property is not used in core messages.
|
||||
|
||||
#### static type
|
||||
|
||||
A string describing the message type sent over the wire.
|
||||
This can be either `"request"` or `"event"`.
|
||||
The primary difference is whether they allow for a response.
|
||||
|
||||
#### static src
|
||||
|
||||
A string or an array of strings describing where this message is allowed to originate from.
|
||||
The available endpoints are `"controller"`, `"host"`, `"instance"`, or `"control"`.
|
||||
Messages sent from a different source will be rejected.
|
||||
|
||||
#### static dst
|
||||
|
||||
A string or array of strings describing where this message may be addressed to.
|
||||
The available endpoints are `"controller"`, `"host"`, `"instance"`, or `"control"`.
|
||||
If the target is multiple hops away it the message will be forwarded towards its destination by the intermediaries.
|
||||
For example a message sent from a control connection an instance will be forwarded by the controller to the relevant host and from the host to the correct instance.
|
||||
Messages sent to a different destination will be rejected.
|
||||
|
||||
#### static jsonSchema
|
||||
|
||||
A JSON schema that specifies what's valid to send in the message.
|
||||
|
||||
#### toJSON
|
||||
|
||||
Optional method to serialize an instance of the class to a JSON object, see the [documentation for `JSON.stringify()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#description) for the semantics of this method.
|
||||
This should return an object that's valid according to `jsonSchema` and accepted by `fromJSON`, and may be omitted if the default serialisation by `JSON.stringify()` already does this.
|
||||
|
||||
#### static fromJSON
|
||||
|
||||
Required function to restore the class from a JSON object.
|
||||
This function is called when the message reaches its destination and is expected to return an identical instance of the class.
|
||||
I.e. the result of `fromJSON(JSON.parse(JSON.stringify(instance)))` should be a complete copy of the message.
|
||||
|
||||
#### static Response
|
||||
|
||||
Optional [JSON sterilisable class](/docs/devs/json-serialisable-classes.md) that specifies the response to the request.
|
||||
Required properties are `jsonSchema` and `fromJSON`.
|
||||
|
||||
## Sending Link Messages
|
||||
|
||||
Link messages are sent by calling the `.sendTo()` method on a connection object with the destination and the data you want to send, where the data is an instance of one of the defined message classes in plugin.messages.
|
||||
For example:
|
||||
|
||||
```js
|
||||
// In an InstancePlugin class
|
||||
async frobnicate() {
|
||||
const response = await this.instance.sendTo("controller", new messages.Frobnicate({
|
||||
verbosity: 2,
|
||||
special: false,
|
||||
}));
|
||||
console.log(response); // { report: [...] }
|
||||
}
|
||||
```
|
||||
|
||||
For classes with `static type = "request"` the send method is async and returns a promise that resolves to the response data received from the target it was sent to, or rejects with an error if the request failed.
|
||||
The destination specification can either be the ID of a particular control, host or instance, or one of the keywords used to send to multiple targets at once.
|
||||
|
||||
Example destinations:
|
||||
|
||||
```js
|
||||
sendTo("controller", message);
|
||||
sendTo("allHosts", message);
|
||||
sendTo("allControls", message);
|
||||
sendTo("allInstances", message);
|
||||
sendTo({ hostId: 123 }, message);
|
||||
sendTo({ instanceId: 123 }, message);
|
||||
sendTo({ controlId: 123 }, message);
|
||||
```
|
||||
|
||||
### 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 controller and host.
|
||||
This is signaled to `ControllerPlugin` classes via the `onPrepareHostDisconnect` hook and to `InstancePlugin` classes via the `onPrepareControllerDisconnect` 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` property of the `HostConnection` class and `Host` class on the controller and host respectively.
|
||||
For example the sending of an event from an `InstancePlugin` class can be stopped while the connection is not connected, not in the dropped state, and not in the process of discunnecting by using the following code:
|
||||
|
||||
```js
|
||||
if (this.host.connected) {
|
||||
this.instance.sendTo("controller", new messages.Frobnicate({ foo: "bar" }));
|
||||
}
|
||||
```
|
||||
|
||||
If the event or request needs to be sent to the controller 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 `onControllerConnectionEvent` and `onHostConnectionEvent` hooks.
|
||||
|
||||
The second connection event which is of lesser importance to respond to is the `drop` connection event served through `onControllerConnectionEvent` for `InstancePlugin` classes and through `onHostConnectionEvent` for `ControllerPlugin` classes.
|
||||
This is raised when the connection between the controller and host in question is lost, most likely due to networking issues.
|
||||
When in the dropped state the host will keep trying to reconnect to the controller 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 either re-established or the session times out.
|
||||
This means that if your plugin sends a lot of events or requests, they can end up being queued up in a buffer and sent out all at once the connection is re-estabished.
|
||||
To avoid this you should be throtteling and/or stopping your requests/events after `drop` has been raised, and continue back as normal when `resume` 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:
|
||||
|
||||
```js
|
||||
const { Counter } = require("@clusterio/lib");
|
||||
|
||||
const fooMetric = new Counter(
|
||||
"clusterio_foo_frobber_foo_metric", "Measures the level of foo",
|
||||
);
|
||||
|
||||
// Somewhere in the controller plugin code
|
||||
fooMetric.inc();
|
||||
```
|
||||
|
||||
This works for controller 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:
|
||||
|
||||
```js
|
||||
const { Counter } = require("@clusterio/lib");
|
||||
|
||||
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.id)).set(someValue);
|
||||
```
|
||||
|
||||
Metrics are automatically registered to the default registry, and this default registry is automatically polled by the controller on hosts.
|
||||
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 host.
|
||||
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 controller 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:
|
||||
|
||||
```js
|
||||
const { Command, CommandTree } = require("@clusterio/lib");
|
||||
const fooFrobberCommands = new CommandTree({
|
||||
name: "foo-frobber", description: "Foo Frobber Plugin commands"
|
||||
});
|
||||
```
|
||||
|
||||
Then commands are added to the the plugin's command tree:
|
||||
|
||||
```js
|
||||
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-controller"` as a part of the links array for it to be accepted by the controller, see [Defining Link Messages](#defining-link-messages) for how to define the messages that can be sent to the controller.
|
||||
|
||||
To have the command tree become part of clusterioctl it needs to be added to the rootCommand tree in `addCommands` callback of the Ctl plugin:
|
||||
|
||||
```js
|
||||
const { BaseCtlPlugin } = require("@clusterio/ctl");
|
||||
|
||||
class CtlPlugin extends BaseCtlPlugin {
|
||||
async addCommands(rootCommand) {
|
||||
rootCommand.add(fooFrobberCommands);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CtlPlugin,
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,10 @@
|
||||
toc:
|
||||
- name: Master Server API
|
||||
- clusterioMaster
|
||||
- name: Type declarations
|
||||
- itemStack
|
||||
- name: Internal modules
|
||||
description: |
|
||||
Here are some helper functions for navigation.
|
||||
- modManager
|
||||
- getFactorioLocale
|
||||
@@ -1,503 +0,0 @@
|
||||
import {
|
||||
defineConfig,
|
||||
globalIgnores,
|
||||
} from "eslint/config";
|
||||
|
||||
import typescriptEslint from "@typescript-eslint/eslint-plugin";
|
||||
import node from "eslint-plugin-node";
|
||||
import globals from "globals";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
import js from "@eslint/js";
|
||||
import stylistic from "@stylistic/eslint-plugin";
|
||||
|
||||
import {
|
||||
FlatCompat,
|
||||
} from "@eslint/eslintrc";
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: import.meta.dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all,
|
||||
});
|
||||
|
||||
export default defineConfig([{
|
||||
plugins: {
|
||||
"@typescript-eslint": typescriptEslint,
|
||||
"@stylistic": stylistic,
|
||||
node,
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.commonjs,
|
||||
},
|
||||
},
|
||||
|
||||
"rules": {
|
||||
"@stylistic/indent": ["error", "tab", {
|
||||
"ignoredNodes": [
|
||||
"JSXFragment", // TODO: Linter crashes if this is removed
|
||||
],
|
||||
"ignoreComments": true,
|
||||
"SwitchCase": 1, // Indent case statements one level from switch
|
||||
}],
|
||||
"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": ["error", "1tbs", {
|
||||
"allowSingleLine": true,
|
||||
}],
|
||||
|
||||
"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": "error",
|
||||
"comma-style": ["error", "last"],
|
||||
"complexity": ["error", { "max": 25 }],
|
||||
"computed-property-spacing": ["error", "never"],
|
||||
"consistent-return": "error",
|
||||
"consistent-this": "error",
|
||||
"curly": "error",
|
||||
"default-case": "error",
|
||||
"default-case-last": "error",
|
||||
"default-param-last": "off",
|
||||
"dot-location": ["error", "property"],
|
||||
|
||||
"dot-notation": ["error", {
|
||||
"allowPattern": "^[a-z]+(_[a-z]+)*$",
|
||||
}],
|
||||
|
||||
"eol-last": "error",
|
||||
"eqeqeq": "error",
|
||||
"func-call-spacing": "error",
|
||||
"func-name-matching": "error",
|
||||
"func-names": "off",
|
||||
|
||||
"func-style": ["error", "declaration", {
|
||||
"allowArrowFunctions": true,
|
||||
}],
|
||||
|
||||
"function-call-argument-newline": "off",
|
||||
"function-paren-newline": "off",
|
||||
|
||||
"generator-star-spacing": ["error", {
|
||||
"before": false,
|
||||
"after": true,
|
||||
}],
|
||||
|
||||
"grouped-accessor-pairs": "error",
|
||||
"guard-for-in": "error",
|
||||
"id-blacklist": "error",
|
||||
"id-length": "off",
|
||||
"id-match": "error",
|
||||
"implicit-arrow-linebreak": "error",
|
||||
|
||||
"indent-legacy": "off",
|
||||
"init-declarations": "off",
|
||||
"jsx-quotes": "error",
|
||||
"key-spacing": "error",
|
||||
"keyword-spacing": "error",
|
||||
"line-comment-position": "off",
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"lines-around-comment": "off",
|
||||
"lines-around-directive": "off",
|
||||
|
||||
"lines-between-class-members": ["error", "always", {
|
||||
"exceptAfterSingleLine": true,
|
||||
}],
|
||||
|
||||
"max-classes-per-file": "off",
|
||||
"max-depth": "error",
|
||||
|
||||
"max-len": ["error", {
|
||||
"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": ["error", "separate-lines"],
|
||||
"multiline-ternary": "off",
|
||||
|
||||
"new-cap": ["error", {
|
||||
"capIsNewExceptions": ["StringEnum", "StringKey"],
|
||||
"capIsNewExceptionPattern": "^Type\\.",
|
||||
}],
|
||||
|
||||
"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": "off",
|
||||
"no-confusing-arrow": "error",
|
||||
"no-console": "error",
|
||||
|
||||
"no-constant-condition": ["error", {
|
||||
"checkLoops": false,
|
||||
}],
|
||||
|
||||
"no-constructor-return": "error",
|
||||
"no-continue": "off",
|
||||
"no-div-regex": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"no-else-return": "error",
|
||||
"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": "error",
|
||||
"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-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-wrappers": "error",
|
||||
"no-octal-escape": "error",
|
||||
"no-param-reassign": "off",
|
||||
|
||||
"no-plusplus": ["error", {
|
||||
"allowForLoopAfterthoughts": true,
|
||||
}],
|
||||
|
||||
"no-proto": "error",
|
||||
"no-restricted-exports": "error",
|
||||
"no-restricted-globals": "error",
|
||||
"no-restricted-imports": "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": ["Event", "Request", "yargs"],
|
||||
}],
|
||||
|
||||
"no-spaced-func": "error",
|
||||
|
||||
"no-tabs": "off",
|
||||
|
||||
"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": "off",
|
||||
"no-useless-backreference": "error",
|
||||
"no-useless-call": "error",
|
||||
"no-useless-computed-key": "error",
|
||||
"no-useless-concat": "error",
|
||||
"no-useless-constructor": "off",
|
||||
"no-useless-rename": "error",
|
||||
"no-useless-return": "error",
|
||||
"no-var": "error",
|
||||
|
||||
"no-void": ["error", {
|
||||
"allowAsStatement": true,
|
||||
}],
|
||||
|
||||
"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": ["error", "after", {
|
||||
"overrides": {
|
||||
"?": "before",
|
||||
":": "before",
|
||||
"||": "before",
|
||||
"&&": "before",
|
||||
"??": "before",
|
||||
},
|
||||
}],
|
||||
|
||||
"padded-blocks": "off",
|
||||
"padding-line-between-statements": "error",
|
||||
"prefer-arrow-callback": "error",
|
||||
"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": "error",
|
||||
"quote-props": "off",
|
||||
|
||||
"quotes": ["error", "double", {
|
||||
"avoidEscape": true,
|
||||
}],
|
||||
|
||||
"radix": ["error", "always"],
|
||||
"require-atomic-updates": "off",
|
||||
"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": "off",
|
||||
"space-before-blocks": "error",
|
||||
"space-before-function-paren": "off",
|
||||
"space-in-parens": ["error", "never"],
|
||||
"space-infix-ops": "off",
|
||||
"space-unary-ops": "error",
|
||||
|
||||
"spaced-comment": ["error", "always", {
|
||||
"line": {
|
||||
"markers": ["/"],
|
||||
},
|
||||
}],
|
||||
|
||||
"switch-colon-spacing": "error",
|
||||
"symbol-description": "error",
|
||||
"template-curly-spacing": ["error", "never"],
|
||||
"template-tag-spacing": "error",
|
||||
"unicode-bom": ["error", "never"],
|
||||
|
||||
"vars-on-top": "error",
|
||||
"wrap-iife": "error",
|
||||
"wrap-regex": "off",
|
||||
"yield-star-spacing": "error",
|
||||
|
||||
"yoda": ["error", "never", {
|
||||
"exceptRange": true,
|
||||
}],
|
||||
|
||||
"node/callback-return": "off",
|
||||
"node/exports-style": "off",
|
||||
"node/file-extension-in-import": "off",
|
||||
"node/global-require": "off",
|
||||
"node/handle-callback-err": "off",
|
||||
"node/no-callback-literal": "off",
|
||||
"node/no-deprecated-api": "off",
|
||||
"node/no-exports-assign": "off",
|
||||
"node/no-extraneous-import": "off",
|
||||
"node/no-extraneous-require": "off",
|
||||
|
||||
"node/no-missing-import": "off",
|
||||
|
||||
"node/no-missing-require": "off",
|
||||
"node/no-mixed-requires": "off",
|
||||
"node/no-new-require": "off",
|
||||
"node/no-path-concat": "off",
|
||||
"node/no-process-env": "off",
|
||||
"node/no-process-exit": "off",
|
||||
"node/no-restricted-require": "off",
|
||||
"node/no-sync": "error",
|
||||
"node/no-unpublished-bin": "off",
|
||||
"node/no-unpublished-import": "off",
|
||||
|
||||
"node/no-unpublished-require": "off",
|
||||
|
||||
"node/no-unsupported-features/es-builtins": "off",
|
||||
"node/no-unsupported-features/es-syntax": "off",
|
||||
"node/no-unsupported-features/node-builtins": "off",
|
||||
"node/prefer-global/buffer": "off",
|
||||
"node/prefer-global/console": "off",
|
||||
"node/prefer-global/process": "off",
|
||||
"node/prefer-global/text-decoder": "off",
|
||||
"node/prefer-global/text-encoder": "off",
|
||||
"node/prefer-global/url-search-params": "off",
|
||||
"node/prefer-global/url": "off",
|
||||
"node/prefer-promises/dns": "off",
|
||||
"node/prefer-promises/fs": "off",
|
||||
"node/process-exit-as-throw": "off",
|
||||
|
||||
"node/shebang": "off",
|
||||
},
|
||||
}, {
|
||||
files: ["{packages,plugins}/**/*.ts?(x)"],
|
||||
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
},
|
||||
|
||||
extends: compat.extends("plugin:@typescript-eslint/recommended"),
|
||||
|
||||
"rules": {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-useless-constructor": "error",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/triple-slash-reference": "off",
|
||||
|
||||
"@typescript-eslint/no-shadow": ["error", {
|
||||
"allow": ["Event", "Request", "yargs"],
|
||||
}],
|
||||
|
||||
// Allow require imports where necessary (dynamic loading, webpack configs, etc.)
|
||||
"@typescript-eslint/no-require-imports": "off",
|
||||
|
||||
"prefer-const": "off",
|
||||
"no-shadow": "off",
|
||||
},
|
||||
}, {
|
||||
files: ["test/**/*.js", "plugins/*/test/**/*.js"],
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.mocha,
|
||||
},
|
||||
|
||||
sourceType: "commonjs",
|
||||
parserOptions: {},
|
||||
},
|
||||
|
||||
"rules": {
|
||||
"prefer-arrow-callback": "off",
|
||||
"no-invalid-this": "off",
|
||||
"node/no-unpublished-import": "off",
|
||||
"node/no-unpublished-require": "off",
|
||||
},
|
||||
}, {
|
||||
files: [
|
||||
"packages/web_ui/src/**/*.ts?(x)",
|
||||
"packages/lib/browser.ts",
|
||||
"packages/create/templates/*/web/**/*.{t,j}s?(x)",
|
||||
"{packages,plugins}/*/web/**/*.ts?(x)",
|
||||
],
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
|
||||
"sourceType": "module",
|
||||
|
||||
parserOptions: {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
"rules": {
|
||||
"node/callback-return": "off",
|
||||
"node/exports-style": "off",
|
||||
"node/file-extension-in-import": "off",
|
||||
"node/global-require": "off",
|
||||
"node/handle-callback-err": "off",
|
||||
"node/no-callback-literal": "off",
|
||||
"node/no-deprecated-api": "off",
|
||||
"node/no-exports-assign": "off",
|
||||
"node/no-extraneous-import": "off",
|
||||
"node/no-extraneous-require": "off",
|
||||
"node/no-missing-import": "off",
|
||||
"node/no-missing-require": "off",
|
||||
"node/no-mixed-requires": "off",
|
||||
"node/no-new-require": "off",
|
||||
"node/no-path-concat": "off",
|
||||
"node/no-process-env": "off",
|
||||
"node/no-process-exit": "off",
|
||||
"node/no-restricted-require": "off",
|
||||
"node/no-sync": "error",
|
||||
"node/no-unpublished-bin": "off",
|
||||
"node/no-unpublished-import": "off",
|
||||
"node/no-unpublished-require": "off",
|
||||
"node/no-unsupported-features/es-builtins": "off",
|
||||
"node/no-unsupported-features/es-syntax": "off",
|
||||
"node/no-unsupported-features/node-builtins": "off",
|
||||
"node/prefer-global/buffer": "off",
|
||||
"node/prefer-global/console": "off",
|
||||
"node/prefer-global/process": "off",
|
||||
"node/prefer-global/text-decoder": "off",
|
||||
"node/prefer-global/text-encoder": "off",
|
||||
"node/prefer-global/url-search-params": "off",
|
||||
"node/prefer-global/url": "off",
|
||||
"node/prefer-promises/dns": "off",
|
||||
"node/prefer-promises/fs": "off",
|
||||
"node/process-exit-as-throw": "off",
|
||||
"node/shebang": "off",
|
||||
},
|
||||
}, {
|
||||
files: ["packages/create/templates/**/*.{t,j}s?(x)"],
|
||||
|
||||
"rules": {
|
||||
"node/no-missing-require": "off",
|
||||
"node/no-missing-import": "off",
|
||||
"@stylistic/indent": "off",
|
||||
"spaced-comment": "off",
|
||||
"comma-spacing": "off",
|
||||
"max-len": "off",
|
||||
"indent": "off",
|
||||
},
|
||||
}, globalIgnores(["**/dist/", "dist/"])]);
|
||||
@@ -1,3 +0,0 @@
|
||||
*
|
||||
!.gitignore
|
||||
!README.md
|
||||
@@ -1,4 +0,0 @@
|
||||
# External plugins directory
|
||||
|
||||
Plugins placed here will have their npm dependencies incorporated in the shared pnpm storage for this repository when `pnpm install` is invoked.
|
||||
Use this to enable `--dev-plugin` to work with plugins developed outside of the Clusterio repository.
|
||||
@@ -0,0 +1,28 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
// const config = require("./../config");
|
||||
|
||||
module.exports = function(config){
|
||||
return {
|
||||
middleware: function (req, res, next) {
|
||||
var token = req.headers['x-access-token'];
|
||||
if(!token) return res.status(401).send({ auth: false, message: 'No token provided.', endpoint: req.route.path});
|
||||
|
||||
jwt.verify(token, config.masterAuthSecret, function(err, decoded) {
|
||||
if(err) return res.status(500).send({ auth: false, message: 'Failed to authenticate token.', endpoint: req.route.path});
|
||||
next();
|
||||
});
|
||||
},
|
||||
check: async function(token){
|
||||
return new Promise((resolve, reject) => {
|
||||
jwt.verify(token, config.masterAuthSecret, function(err, decoded) {
|
||||
if(err){
|
||||
resolve({ok:false, msg:err})
|
||||
} else {
|
||||
resolve({ok:true, msg:"Successfully authenticated"});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
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");
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
var assert = require("assert");
|
||||
var fs = require("fs");
|
||||
|
||||
var fileOps = require("_app/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);
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { getItemDatabase } from "./getItemDatabase";
|
||||
import { getSlaveDatabase } from "./getSlaveDatabase";
|
||||
|
||||
export type db = {
|
||||
items: {[name:string]: any},
|
||||
slaves: Slaves,
|
||||
}
|
||||
|
||||
export function getDatabase(config: ClusterioConfig) {
|
||||
let db: db = {
|
||||
items: getItemDatabase(config),
|
||||
slaves: getSlaveDatabase(config),
|
||||
};
|
||||
return db
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
declare module "getFactorioLocale" {
|
||||
interface callback {
|
||||
(error: Error, factorioLocale?: any): void
|
||||
}
|
||||
export function asObject(factorioDirectory: string, languageCode: string, callback: callback): void
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
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]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
const fileOps = require("_app/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;
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
|
||||
export function getItemDatabase(config: ClusterioConfig): {[name:string]: number | Function} {
|
||||
var items: {[name: string]: number | Function} = {}
|
||||
try{
|
||||
let x = fs.statSync(path.resolve(config.databaseDirectory, "items.json"));
|
||||
console.log(`loading items from ${path.resolve(config.databaseDirectory, "items.json")}`);
|
||||
items = JSON.parse(fs.readFileSync(path.resolve(config.databaseDirectory, "items.json")).toString());
|
||||
} catch (e){
|
||||
items = {};
|
||||
}
|
||||
items.addItem = function addItem(object: item): boolean {
|
||||
if(object.name == "addItem" || object.name == "removeItem") {
|
||||
console.error("Fuck you, that would screw everything up if you named your item that.");
|
||||
return false;
|
||||
} else {
|
||||
if(this[object.name] && Number(this[object.name]) != NaN){
|
||||
this[object.name] = Number(this[object.name]) + Number(object.count);
|
||||
} else {
|
||||
this[object.name] = object.count;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
},
|
||||
items.removeItem = function removeItem(object: item): boolean {
|
||||
if(object.name == "addItem" || object.name == "removeItem") {
|
||||
console.error("Fuck you, that would screw everything up if you named your item that.");
|
||||
return false;
|
||||
} else {
|
||||
if(this[object.name] && Number(this[object.name]) != NaN){
|
||||
this[object.name] = Number(this[object.name]) - Number(object.count);
|
||||
} else {
|
||||
this[object.name] = 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
|
||||
export function getSlaveDatabase(config: ClusterioConfig): Slaves {
|
||||
var slaves: Slaves = {};
|
||||
try{
|
||||
let x = fs.statSync(path.resolve(config.databaseDirectory, "/slaves.json"));
|
||||
console.log(`loading slaves from path.resolve(config.databaseDirectory, "slaves.json")`);
|
||||
slaves = JSON.parse(fs.readFileSync(path.resolve(config.databaseDirectory, "slaves.json"), "utf-8"));
|
||||
} catch (e){
|
||||
slaves = {};
|
||||
}
|
||||
return slaves
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
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`,
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,278 @@
|
||||
/**
|
||||
Provides tools to manage mods in a clusterio install
|
||||
@module modManager
|
||||
*/
|
||||
|
||||
const rightpad = require("right-pad");
|
||||
const asTable = require("as-table").configure({delimiter: ' | '});
|
||||
const moment = require("moment");
|
||||
const syncRequest = require("sync-request");
|
||||
const fs = require("fs-extra");
|
||||
const https = require('follow-redirects').https;
|
||||
const factorioAPI = require("node-factorio-api");
|
||||
const sLog = require("single-line-log").stdout;
|
||||
const query = require('cli-interact').getYesNo;
|
||||
|
||||
const fileOps = require("_app/fileOps");
|
||||
// const config = require("./../../config");
|
||||
|
||||
|
||||
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()))
|
||||
}*/
|
||||
@@ -0,0 +1,116 @@
|
||||
const fs = require("fs-extra");
|
||||
const path = require("path");
|
||||
const git = require("simple-git")("./sharedPlugins");
|
||||
|
||||
|
||||
module.exports = function(config){
|
||||
if(!config) config = require("./../../config");
|
||||
|
||||
return {
|
||||
listPlugins: async function(){
|
||||
let plugins = await this.getPlugins();
|
||||
plugins.forEach(plug => {
|
||||
console.log(`${plug.name}`.padEnd(22).substr(0,22)+` - ${plug.description}`.substr(0,80-22));
|
||||
});
|
||||
},
|
||||
getPlugins: async function(){
|
||||
let directories = await fs.readdir("./sharedPlugins");
|
||||
let returnArray = [];
|
||||
for(let i in directories){
|
||||
let dir = directories[i];
|
||||
let pluginContents = await fs.readdir(path.join("./sharedPlugins", dir));
|
||||
// console.log(pluginContents)
|
||||
if(pluginContents.includes("config.js") || pluginContents.includes("config.json")){
|
||||
try{
|
||||
var pluginConfig = require(path.join("./../../sharedPlugins", dir, "config"));
|
||||
pluginConfig.pluginPath = path.join("./sharedPlugins", dir);
|
||||
}catch(e){
|
||||
console.log(e)
|
||||
}
|
||||
|
||||
// check if plugin is enabled
|
||||
try{
|
||||
await fs.stat(path.join(pluginConfig.pluginPath, "DISABLED"))
|
||||
pluginConfig.enabled = false;
|
||||
}catch(e){
|
||||
pluginConfig.enabled = true;
|
||||
}
|
||||
returnArray.push(pluginConfig);
|
||||
}
|
||||
}
|
||||
return returnArray;
|
||||
},
|
||||
addPlugin: function(path){
|
||||
return new Promise((resolve, reject) => {
|
||||
if(path.includes("git")){ // we are decently sure this is a git repo
|
||||
git.clone(path, data => resolve(data))
|
||||
}
|
||||
});
|
||||
},
|
||||
removePlugin: async function(name){
|
||||
let plugins = await this.getPlugins();
|
||||
for(let i in plugins){
|
||||
let plug = plugins[i];
|
||||
if(plug.name.includes(name)){
|
||||
// console.log(`Removing plugin ${plug.name}`);
|
||||
await fs.remove(plug.pluginPath);
|
||||
return {
|
||||
ok:true,
|
||||
msg:`Deleted plugin ${plug.name}`
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok:false,
|
||||
msg:`Plugin matching "${name}" not found`,
|
||||
}
|
||||
},
|
||||
enablePlugin: async function(name, instance){
|
||||
let plugins = await this.getPlugins();
|
||||
let plugin = plugins[findInArray("name", name, plugins)[0]];
|
||||
if(plugin){
|
||||
let files = await fs.readdir(plugin.pluginPath);
|
||||
if(files.includes("DISABLED")){
|
||||
await fs.unlink(path.join(plugin.pluginPath, "DISABLED"));
|
||||
return {
|
||||
ok:true,
|
||||
msg:`Globally enabled plugin ${name}`,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
ok:true,
|
||||
msg:`Plugin ${name} already enabled`,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
ok:false,
|
||||
msg:`Found no excact match for plugin ${name}`,
|
||||
}
|
||||
}
|
||||
},
|
||||
disablePlugin: async function(name, instance){
|
||||
let plugins = await this.getPlugins();
|
||||
let plugin = plugins[findInArray("name", name, plugins)[0]];
|
||||
if(plugin){
|
||||
await fs.writeFile(path.join(plugin.pluginPath, "DISABLED"), "");
|
||||
return {
|
||||
ok:true,
|
||||
msg:`Plugin ${name} is now disabled on this host`,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
ok:false,
|
||||
msg:`Found no excact match for plugin ${name}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function findInArray(key, value, array){
|
||||
let indexes = [];
|
||||
for(let i in array){
|
||||
if(array[i][key] && array[i][key] === value) indexes.push(i);
|
||||
}
|
||||
return indexes;
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
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 = [
|
||||
"database/linvodb",
|
||||
"sharedMods",
|
||||
"instances",
|
||||
];
|
||||
let downloadList = [
|
||||
// {
|
||||
// url: "http://fbpviewer.trakos.pl/images/spritesheet.json",
|
||||
// file: "static/pictures/spritesheet.js",
|
||||
// callback: function(file){
|
||||
// fs.writeFileSync("static/pictures/spritesheet.js", "export default\n"+fs.readFileSync("static/pictures/spritesheet.js"));
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// url: "http://fbpviewer.trakos.pl/images/spritesheet.png",
|
||||
// file: "static/pictures/spritesheet.png",
|
||||
// },
|
||||
{
|
||||
url: "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,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
--[[
|
||||
|
||||
Copyright 2018 Chrisgbk
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
]]
|
||||
-- MIT License, https://opensource.org/licenses/MIT
|
||||
|
||||
-- Hotpatchable control.lua for scenarios
|
||||
-- Supports multiple simultaneously loaded softmods
|
||||
-- This is a WIP
|
||||
-- Version 1.1 alpha
|
||||
-- probably some performance improvements to be made
|
||||
|
||||
_ENV.debug_settings = {
|
||||
level = 'info',
|
||||
log_to_console_only = false,
|
||||
log_to_RCON = false,
|
||||
log_on_tick = false,
|
||||
}
|
||||
|
||||
local hotpatch_tools = require 'hotpatch.mod-tools'
|
||||
require 'hotpatch.remote-interface'
|
||||
require 'hotpatch.commands'
|
||||
require 'hotpatch.gui'
|
||||
|
||||
-- mod code goes here
|
||||
|
||||
-- This is the Factorio Freeplay scenario
|
||||
-- This code, and the freeplay locale files in the zip file, is the property of Wube
|
||||
-- As this is part of the base mod, the developers have given permission for modders to use/adapt the assets therein, for modding Factorio
|
||||
-- Any other use would require licensing/permission from Wube
|
||||
-- If you don't want to run this, either comment this out before creating your scenario, OR uninstall freeplay at runtime
|
||||
hotpatch_tools.static_mod('freeplay', '1.0.0', [===[
|
||||
local silo_script = require("silo-script")
|
||||
local version = 1
|
||||
|
||||
script.on_event(defines.events.on_player_created, function(event)
|
||||
local player = game.players[event.player_index]
|
||||
player.insert{name="iron-plate", count=8}
|
||||
player.insert{name="pistol", count=1}
|
||||
player.insert{name="firearm-magazine", count=10}
|
||||
player.insert{name="burner-mining-drill", count = 1}
|
||||
player.insert{name="stone-furnace", count = 1}
|
||||
player.force.chart(player.surface, {{player.position.x - 200, player.position.y - 200}, {player.position.x + 200, player.position.y + 200}})
|
||||
if (#game.players <= 1) then
|
||||
game.show_message_dialog{text = {"msg-intro"}}
|
||||
else
|
||||
player.print({"msg-intro"})
|
||||
end
|
||||
silo_script.on_player_created(event)
|
||||
end)
|
||||
|
||||
script.on_event(defines.events.on_player_respawned, function(event)
|
||||
local player = game.players[event.player_index]
|
||||
player.insert{name="pistol", count=1}
|
||||
player.insert{name="firearm-magazine", count=10}
|
||||
end)
|
||||
|
||||
script.on_event(defines.events.on_gui_click, function(event)
|
||||
silo_script.on_gui_click(event)
|
||||
end)
|
||||
|
||||
script.on_init(function()
|
||||
global.version = version
|
||||
silo_script.on_init()
|
||||
end)
|
||||
|
||||
script.on_event(defines.events.on_rocket_launched, function(event)
|
||||
silo_script.on_rocket_launched(event)
|
||||
end)
|
||||
|
||||
script.on_configuration_changed(function(event)
|
||||
if global.version ~= version then
|
||||
global.version = version
|
||||
end
|
||||
silo_script.on_configuration_changed(event)
|
||||
end)
|
||||
|
||||
silo_script.add_remote_interface()
|
||||
silo_script.add_commands()
|
||||
]===])
|
||||
|
||||
-- Multi-file test
|
||||
--[[
|
||||
local files = {}
|
||||
|
||||
files['testfolder.anothertest.test'] = [===[
|
||||
script.on_event(defines.events.on_player_changed_position, function(e) game.print('position changed') end)
|
||||
]===]
|
||||
|
||||
files['testfolder.test'] = [===[
|
||||
require 'anothertest.test'
|
||||
]===]
|
||||
|
||||
files['test'] = [===[
|
||||
require 'testfolder.test'
|
||||
]===]
|
||||
|
||||
hotpatch_tools.static_mod('require-test', '1.0.0', [===[
|
||||
require 'test'
|
||||
]===], files)
|
||||
--]]
|
||||
|
||||
-- end of mod code
|
||||
@@ -0,0 +1,135 @@
|
||||
local hotpatch_tools = require 'hotpatch.mod-tools'
|
||||
hotpatch_tools.static_mod('hotpatch-commands', '1.0.1', [===[
|
||||
--[[
|
||||
|
||||
Copyright 2018 Chrisgbk
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
]]
|
||||
-- MIT License, https://opensource.org/licenses/MIT
|
||||
|
||||
local hotpatch_tools = require 'hotpatch.mod-tools'
|
||||
|
||||
--load private API
|
||||
-- mod installation/uninstallation support functions
|
||||
-- These take a mod NAME as a first argument
|
||||
local install_mod = hotpatch_tools.install_mod
|
||||
local find_installed_mod = hotpatch_tools.find_installed_mod
|
||||
local install_mod_file = hotpatch_tools.install_mod_file
|
||||
local uninstall_mod = hotpatch_tools.uninstall_mod
|
||||
|
||||
-- mod interaction functions
|
||||
-- These take a LOADED INDEX as a first argument, except load_mod, which takes an INSTALLED INDEX
|
||||
local load_mod = hotpatch_tools.load_mod
|
||||
local find_loaded_mod = hotpatch_tools.find_loaded_mod
|
||||
local run_mod = hotpatch_tools.run_mod
|
||||
local reset_mod = hotpatch_tools.reset_mod
|
||||
local reset_mod_events = hotpatch_tools.reset_mod_events
|
||||
local register_mod_events = hotpatch_tools.register_mod_events
|
||||
local unload_mod = hotpatch_tools.unload_mod
|
||||
|
||||
-- internal callbacks when a mod registers events
|
||||
local register_event = hotpatch_tools.register_event
|
||||
local register_nth_tick = hotpatch_tools.register_nth_tick
|
||||
local register_on_tick = hotpatch_tools.register_on_tick
|
||||
|
||||
-- mod bootstrap functions
|
||||
-- These take a LOADED INDEX as a first argument
|
||||
local mod_on_init = hotpatch_tools.mod_on_init
|
||||
local mod_on_load = hotpatch_tools.mod_on_load
|
||||
local mod_on_configuration_changed = hotpatch_tools.mod_on_configuration_changed
|
||||
|
||||
local static_mods = hotpatch_tools.static_mods
|
||||
local console = hotpatch_tools.console
|
||||
local debug_log = hotpatch_tools.debug_log
|
||||
local loaded_mods = hotpatch_tools.loaded_mods
|
||||
local installed_mods = hotpatch_tools.installed_mods
|
||||
|
||||
local help_commands = {
|
||||
help = '/hotpatch help\nYou got this far, you probably know all you need to know',
|
||||
list = '/hotpatch list [type]\nLists \'installed\', \'loaded\' or \'running\' mods',
|
||||
}
|
||||
|
||||
local sub_commands
|
||||
sub_commands = {
|
||||
help = function(player_index, param)
|
||||
local caller = (player_index and game.players[player_index]) or console
|
||||
if (not param) or (param == '') then
|
||||
caller.print('Usage: /hotpatch [command] [parameters]')
|
||||
caller.print('Available commands:')
|
||||
for k, v in pairs(sub_commands) do caller.print(k) end
|
||||
caller.print('Use /hotpatch help [command] for help on a particular command')
|
||||
else
|
||||
local text = help_commands[param]
|
||||
if text then
|
||||
caller.print(text)
|
||||
end
|
||||
end
|
||||
end,
|
||||
list = function(player_index, param)
|
||||
local caller = (player_index and game.players[player_index]) or console
|
||||
if not param or (param == '') then
|
||||
caller.print('Installed mods:')
|
||||
for k, v in pairs(installed_mods) do
|
||||
caller.print(table.concat{'[', k, '] ', v.name, ' ', v.version})
|
||||
end
|
||||
caller.print('Loaded/Running mods:')
|
||||
for k, v in pairs(loaded_mods) do
|
||||
caller.print(table.concat{'[', k, '] ', v.name, ' ', v.version, ' ', (v.loaded and 'loaded') or 'not loaded', ' ', (v.running and 'running') or 'not running'})
|
||||
end
|
||||
else
|
||||
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
local admin_commands = {
|
||||
reinstall = function(player_index, param)
|
||||
local caller = (player_index and game.players[player_index]) or console
|
||||
local mod
|
||||
local installed_index
|
||||
local loaded_index
|
||||
for i = 1, #static_mods do
|
||||
mod = static_mods[i]
|
||||
if mod.name:match('^hotpatch%-.*$') then
|
||||
loaded_index = find_loaded_mod(mod.name)
|
||||
if loaded_index then
|
||||
unload_mod(loaded_index)
|
||||
end
|
||||
install_mod(mod.name, mod.version, mod.code, mod.files)
|
||||
installed_index = find_installed_mod(mod.name)
|
||||
load_mod(installed_index)
|
||||
loaded_index = find_loaded_mod(mod.name)
|
||||
run_mod(loaded_index)
|
||||
mod_on_init(loaded_index)
|
||||
end
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
_ENV.commands.add_command('hotpatch', 'Commands for hotpatch. Run /hotpatch help for details.', function(e)
|
||||
local caller = (e.player_index and game.players[e.player_index]) or console
|
||||
if (not e.parameter) or (e.parameter == '') then
|
||||
sub_commands['help'](e.player_index)
|
||||
return
|
||||
end
|
||||
local sub_command, rest = e.parameter:match('(.-)%s(.*)')
|
||||
if not sub_command then
|
||||
sub_command = e.parameter
|
||||
end
|
||||
|
||||
local f = sub_commands[sub_command]
|
||||
if f then f(e.player_index, rest) end
|
||||
if caller.admin then
|
||||
f = admin_commands[sub_command]
|
||||
if f then f(e.player_index, rest) end
|
||||
end
|
||||
end)
|
||||
|
||||
]===])
|
||||
|
||||
return true
|
||||
@@ -0,0 +1,375 @@
|
||||
local hotpatch_tools = require 'hotpatch.mod-tools'
|
||||
hotpatch_tools.static_mod('hotpatch-gui', '1.0.4', [===[
|
||||
--[[
|
||||
|
||||
Copyright 2018 Chrisgbk
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
]]
|
||||
-- MIT License, https://opensource.org/licenses/MIT
|
||||
|
||||
local hotpatch_tools = require 'hotpatch.mod-tools'
|
||||
local mod_gui = require 'mod-gui'
|
||||
|
||||
--load private API
|
||||
-- mod installation/uninstallation support functions
|
||||
-- These take a mod NAME as a first argument
|
||||
local install_mod = hotpatch_tools.install_mod
|
||||
local find_installed_mod = hotpatch_tools.find_installed_mod
|
||||
local install_mod_file = hotpatch_tools.install_mod_file
|
||||
local uninstall_mod = hotpatch_tools.uninstall_mod
|
||||
|
||||
-- mod interaction functions
|
||||
-- These take a LOADED INDEX as a first argument, except load_mod, which takes an INSTALLED INDEX
|
||||
local load_mod = hotpatch_tools.load_mod
|
||||
local find_loaded_mod = hotpatch_tools.find_loaded_mod
|
||||
local run_mod = hotpatch_tools.run_mod
|
||||
local reset_mod = hotpatch_tools.reset_mod
|
||||
local reset_mod_events = hotpatch_tools.reset_mod_events
|
||||
local register_mod_events = hotpatch_tools.register_mod_events
|
||||
local unload_mod = hotpatch_tools.unload_mod
|
||||
|
||||
-- internal callbacks when a mod registers events
|
||||
local register_event = hotpatch_tools.register_event
|
||||
local register_nth_tick = hotpatch_tools.register_nth_tick
|
||||
local register_on_tick = hotpatch_tools.register_on_tick
|
||||
|
||||
-- mod bootstrap functions
|
||||
-- These take a LOADED INDEX as a first argument
|
||||
local mod_on_init = hotpatch_tools.mod_on_init
|
||||
local mod_on_load = hotpatch_tools.mod_on_load
|
||||
local mod_on_configuration_changed = hotpatch_tools.mod_on_configuration_changed
|
||||
|
||||
local static_mods = hotpatch_tools.static_mods
|
||||
local console = hotpatch_tools.console
|
||||
local debug_log = hotpatch_tools.debug_log
|
||||
local loaded_mods = hotpatch_tools.loaded_mods
|
||||
local installed_mods = hotpatch_tools.installed_mods
|
||||
|
||||
script.on_event(defines.events.on_player_joined_game, function(e)
|
||||
local player = game.players[e.player_index]
|
||||
local top = mod_gui.get_button_flow(player)
|
||||
local left = mod_gui.get_frame_flow(player)
|
||||
local center = player.gui.center
|
||||
|
||||
local button = top['hotpatch-button']
|
||||
if button then
|
||||
button.destroy()
|
||||
end
|
||||
local menu = left['hotpatch-menu']
|
||||
if menu then
|
||||
menu.destroy()
|
||||
end
|
||||
|
||||
local IDE = center['hotpatch-IDE']
|
||||
if IDE then
|
||||
IDE.destroy()
|
||||
end
|
||||
|
||||
local main = center['hotpatch-main']
|
||||
if main then
|
||||
main.destroy()
|
||||
end
|
||||
|
||||
button = top.add{type = 'sprite-button', name = 'hotpatch-button', sprite='utility/heat_exchange_indication', tooltip = 'Hotpatch', style = mod_gui.button_style}
|
||||
end)
|
||||
|
||||
local on_gui_click_handlers
|
||||
local on_gui_selection_state_changed_handlers
|
||||
|
||||
on_gui_click_handlers = {
|
||||
['hotpatch-button'] = function(e)
|
||||
--on_gui_click_handlers['hotpatch-menu.IDE'](e)
|
||||
--do return end
|
||||
local player = game.players[e.player_index]
|
||||
local top = mod_gui.get_button_flow(player)
|
||||
local left = mod_gui.get_frame_flow(player)
|
||||
local center = player.gui.center
|
||||
|
||||
local menu = left['hotpatch-menu']
|
||||
if not menu then
|
||||
menu = left.add{type = 'frame', name = 'hotpatch-menu', direction = 'vertical'}
|
||||
menu.add{type = 'button', name = 'hotpatch-menu.IDE', caption = 'Mod IDE', tooltip = 'Open debugging GUI'}
|
||||
menu.add{type = 'button', name = 'hotpatch-menu.console', caption = 'Mod Console', tooltip = 'Open debugging console'}
|
||||
menu.style.visible = true
|
||||
return
|
||||
end
|
||||
menu.style.visible = not menu.style.visible
|
||||
end,
|
||||
['hotpatch-menu'] = function(e)
|
||||
local player = game.players[e.player_index]
|
||||
local top = mod_gui.get_button_flow(player)
|
||||
local left = mod_gui.get_frame_flow(player)
|
||||
local center = player.gui.center
|
||||
|
||||
local menu = left['hotpatch-menu']
|
||||
menu.style.visible = not menu.style.visible
|
||||
|
||||
on_gui_click_handlers[e.element.name](e)
|
||||
end,
|
||||
['hotpatch-menu.IDE'] = function(e)
|
||||
local player = game.players[e.player_index]
|
||||
local top = mod_gui.get_button_flow(player)
|
||||
local left = mod_gui.get_frame_flow(player)
|
||||
local center = player.gui.center
|
||||
|
||||
local IDE = center['hotpatch-IDE']
|
||||
if not IDE then
|
||||
IDE = center.add{type = 'frame', name = 'hotpatch-IDE', direction = 'vertical', caption = 'Hotpatch IDE', style = mod_gui.frame_style}
|
||||
local top_flow = IDE.add{type = 'flow', name = 'hotpatch-IDE-top', direction = 'horizontal'}
|
||||
top_flow.add{type = 'label', name = 'hotpatch-IDE-mod-label', caption = 'Mod: '}
|
||||
local IDE_dropdown = top_flow.add{type = 'drop-down', name = 'hotpatch-IDE-mod-selector'}
|
||||
top_flow.add{type = 'label', name = 'hotpatch-IDE-mod-version', caption = 'Version: No mod selected'}
|
||||
local IDE_table = IDE.add{type = 'table', name = 'hotpatch-IDE-table', column_count = 2}
|
||||
local files = IDE_table.add{type = 'scroll-pane', name = 'hotpatch-IDE-files', direction='vertical'}
|
||||
files.style.height = 600
|
||||
files.vertical_scroll_policy = 'always'
|
||||
files = files.add{type = 'table', name = 'hotpatch-IDE-files-table', column_count = 1}
|
||||
local code = IDE_table.add{type = 'text-box', name = 'hotpatch-IDE-code'}
|
||||
code.word_wrap = true
|
||||
files.style.width = 400
|
||||
files.style.cell_spacing = 0
|
||||
files.style.top_padding = 0
|
||||
files.style.bottom_padding = 0
|
||||
code.style.width = 600
|
||||
code.style.height = 600
|
||||
IDE.style.visible = false
|
||||
end
|
||||
|
||||
local IDE_dropdown = IDE['hotpatch-IDE-top']['hotpatch-IDE-mod-selector']
|
||||
IDE_dropdown.clear_items()
|
||||
for k, v in ipairs(installed_mods) do
|
||||
IDE_dropdown.add_item(v.name)
|
||||
end
|
||||
|
||||
IDE.style.visible = not IDE.style.visible
|
||||
if IDE.style.visible then
|
||||
player.opened = IDE
|
||||
else
|
||||
player.opened = nil
|
||||
end
|
||||
end,
|
||||
['hotpatch-menu.console'] = function(e)
|
||||
local player = game.players[e.player_index]
|
||||
local top = mod_gui.get_button_flow(player)
|
||||
local left = mod_gui.get_frame_flow(player)
|
||||
local center = player.gui.center
|
||||
|
||||
local console = center['hotpatch-console']
|
||||
if not console then
|
||||
console = center.add{type = 'frame', name = 'hotpatch-console', direction = 'vertical', caption = 'Hotpatch Console'}
|
||||
local top_flow = console.add{type = 'flow', name = 'hotpatch-console-top', direction = 'horizontal'}
|
||||
top_flow.add{type = 'label', name = 'hotpatch-console-mod-label', caption = 'Mod: '}
|
||||
local console_dropdown = top_flow.add{type = 'drop-down', name = 'hotpatch-console-mod-selector'}
|
||||
top_flow.add{type = 'label', name = 'hotpatch-console-mod-version', caption = 'Version: No mod selected'}
|
||||
local bottom_flow = console.add{type = 'flow', name = 'hotpatch-console-bottom', direction = 'vertical'}
|
||||
local 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
|
||||
end
|
||||
|
||||
local console_dropdown = console['hotpatch-console-top']['hotpatch-console-mod-selector']
|
||||
console_dropdown.clear_items()
|
||||
for k, v in ipairs(loaded_mods) do
|
||||
console_dropdown.add_item(v.name)
|
||||
end
|
||||
|
||||
console.style.visible = not console.style.visible
|
||||
if console.style.visible then
|
||||
player.opened = console
|
||||
else
|
||||
player.opened = nil
|
||||
end
|
||||
end,
|
||||
['hotpatch-console-run'] = function(e)
|
||||
local player = game.players[e.player_index]
|
||||
|
||||
local top = mod_gui.get_button_flow(player)
|
||||
local left = mod_gui.get_frame_flow(player)
|
||||
local center = player.gui.center
|
||||
|
||||
local console = center['hotpatch-console']
|
||||
local output = console['hotpatch-console-bottom']['hotpatch-console-output']
|
||||
local input = console['hotpatch-console-bottom']['hotpatch-console-bottom-input']['hotpatch-console-input']
|
||||
local console_dropdown = console['hotpatch-console-top']['hotpatch-console-mod-selector']
|
||||
|
||||
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 code, err = load(input.text, input.text, 't', env)
|
||||
if code then
|
||||
local success, err = pcall(code)
|
||||
if not success then
|
||||
output.text = table.concat{output.text, '\n', err}
|
||||
end
|
||||
else
|
||||
output.text = table.concat{output.text, '\n', err}
|
||||
end
|
||||
|
||||
env.print = old_print
|
||||
env.log = old_log
|
||||
end,
|
||||
['hotpatch-IDE-file'] = function(e)
|
||||
local player = game.players[e.player_index]
|
||||
local top = mod_gui.get_button_flow(player)
|
||||
local left = mod_gui.get_frame_flow(player)
|
||||
local center = player.gui.center
|
||||
|
||||
local element = e.element
|
||||
local previous
|
||||
for k, v in pairs(center['hotpatch-IDE']['hotpatch-IDE-table']['hotpatch-IDE-files']['hotpatch-IDE-files-table'].children) do
|
||||
if table.compare(v.style.font_color or {}, {r=1.0, g=1.0, b=0.0, a=1.0}) then
|
||||
previous = v
|
||||
break
|
||||
end
|
||||
end
|
||||
if previous then
|
||||
previous.style.font_color = {r=1.0, g=1.0, b=1.0, a=1.0}
|
||||
end
|
||||
element.style.font_color = {r=1.0, g=1.0, b=0.0, a=1.0}
|
||||
local file = element.name:match('.-%.(.*)')
|
||||
local selected = center['hotpatch-IDE']['hotpatch-IDE-top']['hotpatch-IDE-mod-selector']
|
||||
local mod_name = selected.items[selected.selected_index]
|
||||
local code = center['hotpatch-IDE']['hotpatch-IDE-table']['hotpatch-IDE-code']
|
||||
local index = find_installed_mod(mod_name)
|
||||
if index then
|
||||
local mod = installed_mods[index]
|
||||
if file == 'control' then
|
||||
code.text = mod.code
|
||||
else
|
||||
code.text = mod.files[file]
|
||||
end
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
script.on_event(defines.events.on_gui_click, function(e)
|
||||
local element = e.element
|
||||
if element.valid then
|
||||
local name = element.name:match('([^%.]*)%.?.-')
|
||||
local handler = on_gui_click_handlers[name]
|
||||
if handler then handler(e) end
|
||||
end
|
||||
end)
|
||||
|
||||
on_gui_selection_state_changed_handlers = {
|
||||
['hotpatch-console-mod-selector'] = function(e)
|
||||
local player = game.players[e.player_index]
|
||||
local top = mod_gui.get_button_flow(player)
|
||||
local left = mod_gui.get_frame_flow(player)
|
||||
local center = player.gui.center
|
||||
|
||||
local element = e.element
|
||||
local name = element.items[element.selected_index]
|
||||
if name then
|
||||
local index = find_loaded_mod(name)
|
||||
if index then
|
||||
local mod = loaded_mods[index]
|
||||
local version_label = center['hotpatch-console']['hotpatch-console-top']['hotpatch-console-mod-version']
|
||||
version_label.caption = 'Version: ' .. mod.version
|
||||
local console = center['hotpatch-console']
|
||||
local output = console['hotpatch-console-bottom']['hotpatch-console-output']
|
||||
output.text = ''
|
||||
end
|
||||
end
|
||||
end,
|
||||
['hotpatch-IDE-mod-selector'] = function(e)
|
||||
local player = game.players[e.player_index]
|
||||
local top = mod_gui.get_button_flow(player)
|
||||
local left = mod_gui.get_frame_flow(player)
|
||||
local center = player.gui.center
|
||||
|
||||
local element = e.element
|
||||
|
||||
local name = element.items[element.selected_index]
|
||||
if name then
|
||||
local index = find_installed_mod(name)
|
||||
if index then
|
||||
local mod = installed_mods[index]
|
||||
local version_label = center['hotpatch-IDE']['hotpatch-IDE-top']['hotpatch-IDE-mod-version']
|
||||
version_label.caption = 'Version: ' .. mod.version
|
||||
local list = center['hotpatch-IDE']['hotpatch-IDE-table']['hotpatch-IDE-files']['hotpatch-IDE-files-table']
|
||||
list.clear()
|
||||
local file = list.add{type = 'label', style = 'hoverable_bold_label', caption = 'control', name = 'hotpatch-IDE-file.control'}
|
||||
file.style.bottom_padding = 0
|
||||
file.style.top_padding = 0
|
||||
for k, v 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.style.visible then
|
||||
player.play_sound{path = "utility/gui_click"}
|
||||
end
|
||||
element.style.visible = false
|
||||
elseif element.name == 'hotpatch-console' then
|
||||
if element.style.visible then
|
||||
player.play_sound{path = "utility/gui_click"}
|
||||
end
|
||||
element.style.visible = false
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
]===])
|
||||
|
||||
return true
|
||||
@@ -0,0 +1,293 @@
|
||||
local hotpatch_tools = require 'hotpatch.mod-tools'
|
||||
hotpatch_tools.static_mod('hotpatch-remote-interface', '1.0.2', [===[
|
||||
--[[
|
||||
|
||||
Copyright 2018 Chrisgbk
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
]]
|
||||
-- MIT License, https://opensource.org/licenses/MIT
|
||||
|
||||
local hotpatch_tools = require 'hotpatch.mod-tools'
|
||||
|
||||
--load private API
|
||||
-- mod installation/uninstallation support functions
|
||||
-- These take a mod NAME as a first argument
|
||||
local install_mod = hotpatch_tools.install_mod
|
||||
local find_installed_mod = hotpatch_tools.find_installed_mod
|
||||
local install_mod_file = hotpatch_tools.install_mod_file
|
||||
local uninstall_mod = hotpatch_tools.uninstall_mod
|
||||
|
||||
-- mod interaction functions
|
||||
-- These take a LOADED INDEX as a first argument, except load_mod, which takes an INSTALLED INDEX
|
||||
local load_mod = hotpatch_tools.load_mod
|
||||
local find_loaded_mod = hotpatch_tools.find_loaded_mod
|
||||
local run_mod = hotpatch_tools.run_mod
|
||||
local reset_mod = hotpatch_tools.reset_mod
|
||||
local reset_mod_events = hotpatch_tools.reset_mod_events
|
||||
local register_mod_events = hotpatch_tools.register_mod_events
|
||||
local unload_mod = hotpatch_tools.unload_mod
|
||||
|
||||
-- internal callbacks when a mod registers events
|
||||
local register_event = hotpatch_tools.register_event
|
||||
local register_nth_tick = hotpatch_tools.register_nth_tick
|
||||
local register_on_tick = hotpatch_tools.register_on_tick
|
||||
|
||||
-- mod bootstrap functions
|
||||
-- These take a LOADED INDEX as a first argument
|
||||
local mod_on_init = hotpatch_tools.mod_on_init
|
||||
local mod_on_load = hotpatch_tools.mod_on_load
|
||||
local mod_on_configuration_changed = hotpatch_tools.mod_on_configuration_changed
|
||||
|
||||
local static_mods = hotpatch_tools.static_mods
|
||||
local console = hotpatch_tools.console
|
||||
local debug_log = hotpatch_tools.debug_log
|
||||
local loaded_mods = hotpatch_tools.loaded_mods
|
||||
local installed_mods = hotpatch_tools.installed_mods
|
||||
|
||||
local remote_interface = {}
|
||||
|
||||
--IMPORTANT: WHEN COPY PASTING CODE TO CONSOLE THE CODE MUST HAVE SINGLE LINE COMMENTS REMOVED
|
||||
-- FACTORIO CONSOLE STRIPS LINEFEEDS WHICH MAKES ALL THE CODE BECOME COMMENTED OUT
|
||||
--TODO: gate these behind admin permissions; if (game.player and game.player.admin) should work for console commands?
|
||||
|
||||
debug_log({'hotpatch-info.remote-installing'})
|
||||
|
||||
remote_interface['install'] = function(mod_name, mod_version, mod_code, mod_files, only_install)
|
||||
-- this installs a new mod and runs on_init, then registers events
|
||||
-- Note that mods may expect that certain events haven't been called yet when their on_init is ran
|
||||
-- This may prevent them from functioning properly, without manually calling the events they expect
|
||||
-- examples: on_player_created
|
||||
local caller = game.player or console
|
||||
if caller.admin then
|
||||
if mod_files then
|
||||
for k,v 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('WARNING: mod already exists: ' .. mod_name .. ' ' .. old_version)
|
||||
debug_log('WARNING: reinstalling mod in-place: ' .. mod_name .. ' ' .. mod_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)
|
||||
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
|
||||
local mod = loaded_mods[loaded_index]
|
||||
local version = mod.version
|
||||
debug_log('WARNING: mod already loaded: ' .. mod_name .. ' ' .. version)
|
||||
debug_log('WARNING: reinitializing: ' .. mod_name .. ' ' .. version)
|
||||
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,v 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)
|
||||
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
|
||||
|
||||
--TODO: most of this function
|
||||
remote_interface['clean'] = function(mod_name)
|
||||
-- Removes ALL mod data and reinitializes
|
||||
-- doesn't remove things a mod may add like surfaces, etc
|
||||
local caller = game.player or console
|
||||
if caller.admin then
|
||||
mod_reset(mod_name)
|
||||
run_mod(mod_name)
|
||||
mod_on_init(mod_name)
|
||||
mod_on_configuration_changed(mod_name, {mod_changes = {mod_name={new_version=mod_version}}})
|
||||
else
|
||||
caller.print('You must be an admin to run this command.')
|
||||
end
|
||||
end
|
||||
|
||||
remote_interface['uninstall'] = function(mod_name)
|
||||
-- Uninstalls a mod
|
||||
local caller = game.player or console
|
||||
if caller.admin then
|
||||
uninstall_mod(mod_name)
|
||||
else
|
||||
caller.print('You must be an admin to run this command.')
|
||||
end
|
||||
end
|
||||
|
||||
remote.add_interface('hotpatch', remote_interface)
|
||||
|
||||
debug_log({'hotpatch-info.complete', {'hotpatch-info.remote-installing'}})
|
||||
|
||||
]===])
|
||||
|
||||
--[[
|
||||
|
||||
remote.call('hotpatch, 'install', 'test', '1.0.0', [===[
|
||||
script.on_event(defines.events.on_player_changed_position, function(e)
|
||||
game.print('changed position')
|
||||
end
|
||||
]===]
|
||||
|
||||
local updated_code = ....
|
||||
remote.call('hotpatch', 'update', 'test', '1.0.1', updated_code)
|
||||
|
||||
|
||||
]]
|
||||
|
||||
return true
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Hierdie is Factorio vryspel. Jou taak is om 'n vuurpyl in die buitenste ruimte in te stuur. Doen dit deur die bou van 'n Vuurpyl Silo en van daar af 'n vuurpyl te lanseer met a satelliet aan boord. Jy sal baie gevorderde tegnologiëe moet navors om die Vuurpyl Silo te ontsluit. Begin klein, werk jou pad op met outomatisering en moenie vergeet om jouself te beskerm teen die inboorlinge nie.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=.نييلصألا ناكسلا نم كسفن ةيامح ىسنت الو ةياهنلا ىلإ لصتل كتقيرطب لمعأو ،رفصلا نم أدبتس .خيراوصلا عماوص حتف لجأ نم ةمدقتم ايجولونكت نع ثحبلل جاتحت فوس .ةيعانصلا رامقألاب خيراوصلا قالطإو خيراوصلا عماوص ءانب قيرط نع كلذب مايقلا عيطتست .ءاضفلل اخوراص قلطت نا يه كتمهم .Factorio ـل رحلا بعللا وه اذه
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Гэта рэжым свабоднай гульні Factorio. Вашая мэта - запусціць ракету ў космас. Дзеля гэтага вам трэба пабудаваць ракетную шахту і саму ракету з спадарожнікам. Вы мусіце даследваць перадавыя тэхналогіі, каб навучыцца будаваць ракетную шахту. Пачынайце з малога, працуйце над аўтаматызацыяй вытворчасці, даследуйце новыя тэхналогіі і не забывайцеся на абарону ад "тутэйшых"!
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Това е режимът на свободна игра. Задачата ви е да изстреляте ракета в космоса. За това ще е нужно да построите ракетна шахта и изстреляте ракетата със спътник. Ще трябва да проучите развити технологии, които ще отключат ракетната шахта. Започнете с малко и постепенно автоматизирайте процеса, като не забравяте да се защитавате от местните форми на живот.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Aquest és el mode lliure del Factorio. La vostra tasca és construir un coet i enviar-lo a l'espai. Ho haureu de fer construint un centre de coets espacials i llençant un coet amb un satèl·lit. Necessitareu investigar tecnologies avançades per poder-ho fer. Aneu pas a pas, avanceu cap a l'automatització i no oblideu protegir-vos dels natius.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Toto je volná hra Factorio. Tvým úkolem je vypustit raketu do vesmíru. Postav raketové silo a vypusť raketu se satelitem. Pro odemknutí raketového sila budeš muset vyzkoumat pokročilé technologie. Začni v malém, postupuj s automatizací a nezapomeň se chránit před domorodci.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Det her er Factorio fri leg. Din opgave er at affyre en raket for at udforske rummet. For at gøre det skal du bygge en raket silo og affyre en raket med en satellit i. Du bliver nød til at udforske avancerede teknologier for at at kunne bygge en raket silo. Start småt, og arbejd din vej op med hjælp af automatisering, glem ikke at beskytte dig fra de indfødte.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Dies ist das freie Spiel von Factorio. Deine Aufgabe ist es, eine Rakete in den Weltraum zu starten. Dazu musst Du ein Raketensilo bauen und eine Rakete mit einem Satelliten starten. Du wirst erweiterte Technologien erforschen müssen, um das Raketensilo freizuschalten. Fange klein an, arbeite Dich durch Automatisierung hoch und vergiss nicht, Dich vor den Einheimischen zu schützen.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Αυτό είναι το ελεύθερο παιχνίδι του Factorio. Ο στόχος σου είναι να εκτοξεύσεις έναν πύραυλο στο διάστημα. Αυτό το κάνεις κατασκευάζοντας ένα σιλό πυραύλου και εκτοξεύοντας έναν πυραύλο με δορυφόρο. Θα πρέπει να ερευνήσεις προηγμένες τεχνολογίες για να ξεκλειδώσεις το σιλό πυραύλου. Ξεκινάς περιορισμένος, δούλεψε με αυτοματισμούς για την άνοδο σου και μην ξεχνάς να προστατέψεις τον εαυτό σου από τους ντόπιους.
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
msg-intro=This is the Factorio freeplay. Your task is to launch a rocket into space. Do this by constructing a Rocket Silo and launching a rocket with a satellite. You will need to research advanced technologies in order to unlock the Rocket Silo. Start small, work your way up with automation and don't forget to protect yourself from the natives.
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
[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-loaded=mod already loaded
|
||||
reinitializing=reinitializing...
|
||||
already-exists=mod already exists
|
||||
reinstalling=reinstalling mod in-place
|
||||
|
||||
[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
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Tio estas la Factorio Libera Ludado. Via tasko estas ĵeti raketon en spacon. Fari tiun per konstruanta Raketo Silo kaj ĵeti raketon kun satelito. Vi bezonos esplori altnivelaj teknologioj por malŝlosi la Raketo Silo. Start malgranda, labori vian vojon supren per aŭtomatigo kaj ne forgesu protekti vin mem de la indiĝenoj.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Este es el modo de juego libre de Factorio. Tu tarea es la de lanzar un cohete al espacio. Logra esto construyendo un Silo de Cohetes y lanzando un cohete con un satélite. Necesitarás investigar tecnologías avanzadas para poder desbloquear el Silo de Cohetes. Comienza poco a poco, trabaja en tu sistema de automatización y no te olvides de protegerte de los nativos.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=See on Factorio vabamäng. Sinu eesmärgiks on saata rakett avakosmosesse. Seda saad teha ehitades Raketi Silotorni ja lennutades Raketti koos satelliidiga. Selleks pead õppima selgeks keerukad tehnoloogiaharud, et Raketi Silotorni lahti lukustada. Alusta väikselt ning liigu edasi automatiseerimise abil ja ära unusta ennast kaitsta kohalike eluvormide eest.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Tämä on Factorion vapaapelimuoto. Tavoitteesi on lähettää satelliitti ulkoavaruuteen rakentamalla rakettisiilo ja laukaisemalla raketti. Rakettisiilon voit rakentaa vasta, kun olet kehittänyt siihen tarvittavat huipputeknologiat. Aloita pienestä ja automatisoi kaikki tarvittava, äläkä unohda suojella itseäsi alkuasukkailta.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Il s'agit du mode de jeu libre de Factorio. Votre tâche consiste à lancer une fusée dans l'espace. Ceci en construisant un silo à fusée, et en lançant un satellite avec la fusée. Vous aurez besoin de rechercher des technologies de pointe afin de débloquer le silo. Commencez petit, évoluez en automatisant et n'oubliez pas de vous protéger contre les autochtones.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=.םיימוקמה םירוציה ינפמ םכמצע לע ןגהל וחכשת לאו ,יטמוטוא ןוכימ תועצמאב םכלש הישעתה תא וביחרה ,ןטקב וליחתה .ןקתמה תיינב תא רשפאל ידכ תומדקתמ תויגולונכט רוקחל םכילע היהי .ןייוול ליכמה ליט ותועצמאב רגשלו םיליט רוגיש ןקתמ תונבל םכילע .ללחל ליט רגשל איה םכתמישמ .ויִרֹוטְקַפ לש ישפוחה שיחרתה והז
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Ez a Factorio szabad játék módja. A feladatod az, hogy kilőj egy rakétát az űr felderítésére. Ehhez egy Rakétasilót kell építened, melyből aztán egy műholddal felszerelt rakétát kell kilőnöd. A Rakétasiló kifejlesztéséhez különféle technológiákat kell fejlesztened. Kezdj kicsiben és küzdd fel magad az automatizálásban. Ne felejts el védekezni a honos fajok ellen.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Questa è la modalità libera di Factorio. Il tuo obiettivo è quello di lanciare il razzo nello spazio. Completalo costruendo un Silo per Razzi e lanciando il razzo con il satellite. Avrai bisogno di ricercare tecnologie avanzate per riuscire a sbloccare il Silo per Razzi. Inizia in piccolo, espandi con l'automazione e non dimenticare di proteggerti dai nativi.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=これは、Factorioのフリープレイです。ロケットサイロを建造し、衛星を載せたロケットを宇宙へ打ち上げるのがあなたの任務です。ロケットサイロをアンロックするためには高度な技術研究をする必要があります。小規模に始め、自動化しつつ進み、原住生物から身を守ることも忘れないでください。
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Factorio 자유플레이입니다. 당신의 목표는 우주로 로켓을 발사하는 것입니다. 로켓 격납고를 건설하고 인공위성을 탑재한 로켓을 발사하여 이 목표를 달성하세요. 로켓 격납고의 잠금을 해제하기 위해 고급 기술들을 연구할 필요가 있습니다. 작은 것부터 시작해 자동화로 차근차근 발전하고, 토착 생물로부터 자신을 보호하는 것도 잊지 마세요.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Tai Factorio laisvas žaidimas. Jūsų užduotis - paleisti raketą į kosmosą. Tam reikia pastatyti Raketos šachtą ir paleisti raketą su palydovu. Reikės atrasti pažangias technologijas kad atrakintumėte Raketos šachtą. Pradėkite palengva, tobulinkite automatiką ir neužmirškite apsisaugoti nuo vietinių.
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
msg-intro=Šis ir Factorio brīvais režīms. Tavs uzdevums ir palaist raķeti kosmosā. Lai to izdarītu, tev vajadzēs uzbūvēt palaišanas platformu un palaist raķeti ar satelītu. Tev būs jāizpēta sarežģītas tehnoloģijas, lai atbloķētu palaišanas platformu. Sāc no mazumiņa, strādā un neaizmirsti pasargāt sevi no šīs planētas iemītniekiem...
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Dit is Factorio vrij spel. Je taak is om een raket te lanceren in de ruimte. Bouw hiervoor een raket-silo en lanceer een raket met een satelliet. Je zult geavanceerde technologieën moeten onderzoeken om de raket-silo vrij te spelen. Begin op kleine schaal, breidt uit en automatiseer je fabriek en vergeet niet jezelf te beschermen tegen de inheemse bewoners.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=Dette er Factorio frispilling. Din oppgave er å sende en rakett til verdensrommet. Bygg en rakettsilo og send opp en rakett med en satellitt. Du blir nødt til å utforske avanserte teknologier for å få tilgang til rakettsiloen. Begynn i det små, jobb deg oppover med hjelp av automatisering, men ikke glem å beskytte deg mot de innfødte.
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
msg-intro=To jest swobodna rozgrywka Factorio. Twoim zadaniem jest wysłanie rakiety w kosmos. Skonstruuj silos rakietowy i wystrzel z niego rakietę z satelitą. Żeby odblokować silos rakietowy będziesz musiał odkryć zaawansowane technologie. Rozpocznij od podstaw, rozwijaj się wykorzystując automatyzację i nie zapomnij o obronie przed rodzimymi mieszkańcami planety.
|
||||
|
||||
|
||||