Compare commits

..
460 Commits
Author SHA1 Message Date
Hornwitser eee4c69575 v2.0.0-alpha.1 2020-10-28 15:02:13 +01:00
Hornwitser adbbdef1a8 Merge pull request #327 from Hornwitser/add-package-readmes
Add package readmes and update project
readme file.
2020-10-28 13:32:04 +01:00
Hornwitser 36e5606e25 Add section on configuring the master server
Add detailed instructions for setting the https port the master server
runs on, setting the external_address option and how to set up a reverse
proxy to the master server.
2020-10-28 01:48:59 +01:00
Hornwitser 5fb7a33199 Link slave and ctl docs in README
Link to the slave package README and the managing-a-cluster document in
the sections for slave and clusterctl.
2020-10-28 01:39:43 +01:00
Hornwitser db0a3fb85e Drop support for Node.js v11
Drop claims of supporting the EOL v11 branch of Node.js.
2020-10-28 00:58:01 +01:00
Hornwitser 1215bbf3ab Rewrite introduction to Clusterio
Rewrite the woefully out of date description of Clusterio in the
readme to give a better sense of what Clusterio is and the available
plugins.
2020-10-28 00:58:01 +01:00
Hornwitser 64ac5a181a Fix typo in managing-a-cluster.md filename. 2020-10-28 00:58:01 +01:00
Hornwitser 4834c289ff Add README.md files to packages
Add README with basic descriptions of each package to all of the
packages in this repository.
2020-10-28 00:58:01 +01:00
Hornwitser 5fffc282fe Merge pull request #332 from Hornwitser/web-plugins
Implement plugin loading in web interface
2020-10-27 21:57:42 +01:00
Hornwitser 6dbf29153e Implement plugin loading in web interface
Add loading of plugin info to the web interface via webpack Module
Federation.
2020-10-27 18:24:45 +01:00
Hornwitser 542d8a798f Merge pull request #326 from Hornwitser/publish-fixes
Publish fixes
2020-10-26 15:50:53 +01:00
Hornwitser 7d8b55adb6 Use less strict matching for lib dependency
Stop requiring exact version match for the clusterio/lib dependency in
master, slave and ctl to make doing alpha releases simpler.
2020-10-26 15:24:29 +01:00
Hornwitser 1eca2ec6e6 Add missing ajv dependency to clusterio/lib
The lib/schema module uses ajv for compiling JSON Schema validators but
for some reason ajv was not part of the dependency list in its
package.json.
2020-10-26 15:17:01 +01:00
Hornwitser 9ee3fe6c37 Move gitignore for web build out of package
Mave the ignore entry for packages/master/static out of
packages/master/.gitignore to avoid npm using it and causing the web
build to be excluded from the package.
2020-10-24 16:46:02 +02:00
Hornwitser d96fb75703 Mark packages public
Add the required publishConfig.access: public for publishing public
packages in an ord on npm.
2020-10-24 14:43:42 +02:00
Hornwitser 20c7b613f2 Use lerna's pre-publish version format
Use -alpha.0 instead of -alpha0 as the pre-publish version as this is
the format lerna uses with the --canary option to publish.
2020-10-24 14:31:49 +02:00
Hornwitser 59436e7f10 Add notice in changelog about uploading to npm 2020-10-24 14:18:17 +02:00
Hornwitser a614ef8405 Merge pull request #325 from Hornwitser/lint-2
Lint fixes 2
2020-10-24 14:00:41 +02:00
Hornwitser 6d005505ce Fix brace style in lib/prometheus 2020-10-23 21:22:45 +02:00
Hornwitser ceade431ef Convert dole code to follow project style 2020-10-23 21:22:45 +02:00
Hornwitser 8586dfb95f Fix capitalization of constructors
Enforce new-cap rule and fix capitalization and mistakes related to
constructors.
2020-10-23 21:22:45 +02:00
Hornwitser 00a1f6aea7 Remove variable shadowing
Refactor to remove all no-shadow warnings.
2020-10-23 21:22:45 +02:00
Hornwitser e5e82ba2fb Remove inventoryImports plugin
Remove defunct and coding horror inventoryImports.  The functionality
provided in it should be added as an optional extra to subspace_storage.
2020-10-23 21:22:45 +02:00
Hornwitser 24397b75cc Prefix clusterio/lib imports with lib
Use lib* prefix for the modules impored from the lib package to help
distinguish them more from ordinary variables.  Fix up the naming
convention for files to be consistently lowercase_underscore for
JavaScript files.
2020-10-23 21:22:43 +02:00
Hornwitser fbbaf3dd3a Alter no-mixed-operators rule
Change the rule for mixed operators to allow basic arithmetic and
boolean logic.
2020-10-23 19:05:12 +02:00
Hornwitser a84f2d5eb6 Use strict equality checking
Remove usage of == and != in favour of === and !==.
2020-10-23 19:05:12 +02:00
Hornwitser ee81fbb7f6 Merge pull request #323 from Hornwitser/plugin-restructure
Convert plugins to node packages
2020-10-23 19:04:17 +02:00
Hornwitser 65f0845398 Fix plugin modules not loading
Fix the path to plugin modules using the old plugins dir instead of
resolving to the actual path of the plugin, which in turn caused no
plugin modules to be loaded.
2020-10-23 18:13:13 +02:00
Hornwitser 7968d89345 Use peerDependency for plugins
Avoid any potential for accidentally installing multiple copies of
clusterio/lib by specifiying it as a peer dependency instead of a
regular dependency.
2020-10-23 17:01:43 +02:00
Hornwitser 0ae520b5cc Fix plugin add command incorrectly resolving path
Fix the plugin add command not storing the resolved path for relative
paths passed to it into plugin-list.json and incorrectly resolving any
path containing ./ or ../ instead of only paths starting with these.
2020-10-23 16:59:20 +02:00
Hornwitser 1b0b245484 Fix plugin version not being loaded
Load the version of the plugin from its package.json file and document
this requirement in the writing plugins document.
2020-10-23 16:57:49 +02:00
Hornwitser c5a14db8ea Fix path for export test
Fix the path tested for the export-data command test incorrectly using
the project root instead of temp/test.
2020-10-22 16:38:46 +02:00
Hornwitser 7bba6cbde6 Add basic error check in build_mod.js
Check if the additional file to add exists before trying to pack it.
Note that there may be other ways reading the files could break, but
this should suffice for most cases of getting it wrong.
2020-10-21 21:38:24 +02:00
Hornwitser b2408d54ec Convert plugins to node packages
Setup package.json for the plugins and add them back to tests now that
they are compatible again.
2020-10-21 21:37:26 +02:00
Hornwitser f06d3509e6 Move lua code to slave package and fix paths
Move the export and clusterio mod code to the slave package and fix the
path used for them.  Refactor tests to use temp/tests/ as the cwd when
running integration tests in order to ensure paths to lua code are
correctly handled.
2020-10-21 21:26:44 +02:00
Hornwitser 18e609f3fe Allow plugins to be installed as npm packages
Add a separate plugin-list.json file for storing which plugins are
available and the path to pass to require in order to load it.  This
allows plugins to be published and installed as npm packages.  And also
replaces the old method of installing plugins by putting them in the
plugins folder.
2020-10-21 19:35:24 +02:00
Hornwitser 2a7bc75929 Move generation of auth secret to master startup
Move the code generating the master authentication secret from the
inital value function of the master.auth_secret config entry to the
master startup.  Having the authentication secret generated in the
config library poses issues with using the config library in browser.
2020-10-21 17:58:30 +02:00
Hornwitser 34a0ce4dc3 Merge pull request #321 from Hornwitser/instance-status
Rework instance status
2020-09-29 11:34:55 +02:00
Hornwitser 003b035abd Send instance status in listInstances request
Send the status of the instance in the repsonse to the listInstances
request and use this status in the integration tests to check if the
instance was started/stopped.
2020-09-29 01:28:00 +02:00
Hornwitser b82de23876 Extend instance statuses
Add the statuses unknown and unassigned for when the status has not yet
been set by a slave and replace the initialized status with starting,
creating_save and exporting_data.
2020-09-29 01:01:19 +02:00
Hornwitser 363bb54902 Merge pull request #320 from Hornwitser/web-interface
Import new web interface
2020-09-28 18:09:41 +02:00
Hornwitser dfaf74aac5 Supress webpack asset size warning
Set threshold for webpack's asset size warning to 2 MB due to Ant Design
being large.
2020-09-28 05:57:10 +02:00
Hornwitser 86b8775df0 Fix modals stuck after web interface import
Fix the modals for adding new entries to instances, users, and roles not
being closable due to a missing bind of the close method.
2020-09-28 05:52:31 +02:00
Hornwitser bccc89fa0b Finish linting web interface
Fix remaining linter errors detected in web interface code.
2020-09-28 05:52:31 +02:00
Hornwitser 03b7d22ba6 Use consistent semi-colon placment
Add semicolons to web interface code to be consistent with project
standard.
2020-09-28 05:52:29 +02:00
Hornwitser 8dce1dd0fd Fix web interface indents
Use project standard of tabs for indents.
2020-09-28 05:51:15 +02:00
Hornwitser 94200433cc Serve interface on all routes for the interface
Add all routes the web interface is served on to the express app and
serve the index.html file on.  Due to the index html not knowing which
path it's hosted under, a path to the root of the web interface is
replaced using string substitution from the master server.
2020-09-28 05:51:11 +02:00
Hornwitser 392a7655c4 Use property for calculating handler name
Replace usage of .constructor.name with a separate property to avoid
issue with JavaScript terser mangling the class name during minification
for the web build.
2020-09-28 04:23:45 +02:00
Hornwitser f1f9b4d020 Close webpack middleware on shutdown
Call the .close method on the webpack-dev-middleware in order to
properly shut it down.
2020-09-28 04:23:45 +02:00
Hornwitser 701ff87bcd Implement webpack build for web interface
Set up webpack for the master server's web interface and fix the issues
from the imported interface source files.  The .jsx files in
package/master/web are bundled together into package/master/static and
served by the master server via express.
2020-09-28 04:23:45 +02:00
Danielv123 2382f4bbdc Import web interface from clusterio/clusterioWeb
Import the source files in use for the web interface, excluding the
copied and modified lib/link files.
2020-09-28 04:19:00 +02:00
Hornwitser f318f8826b Adapt config and link library to web
Make the config and link libraries compatible with packing for web with
webpack.  This would fail due to ws not being web compatible and config
using a node function not available in the web shim for util.
2020-09-28 04:19:00 +02:00
Hornwitser fbf2843f5b Remove old web interface 2020-09-22 22:06:03 +02:00
Daniel bca949f7be Merge pull request #318 from Hornwitser/restructure-project
Restructure project
2020-09-21 11:57:43 -07:00
Hornwitser 5e410a6ac3 Use full path when referencing files in documentation
Include the packages/ component in paths referenced in the documentation
to make it clear where the files are located in the repository.

[skip travis]
2020-09-21 19:57:52 +02:00
Hornwitser 0cca9ed22c Mark user creation tests as slow
Mark the "should not create the user if not instructed to" and "should
create the user if instructed to" tests as slow as these take more than
2 seconds to execute on travis.
2020-09-21 19:27:24 +02:00
Hornwitser 6621e37e23 Do not use ci in travis environment
Disable ci mode for lerna in travis environment as this requires
package-lock.json files to be commited to the repository and we do not
for the time being do this.
2020-09-21 18:51:21 +02:00
Hornwitser bd8f685a18 Fix JSDoc module references
Fix module names, which are incorrectly deduced by JSDoc, and references
to master, slave and ctl modules that are now in separate packages.
2020-09-21 18:00:07 +02:00
Hornwitser 3485c9ac68 Rename clusterctl to clusterioctl
Rename the clusterctl command line utility to clusterioctl in order to
be consistent with the naming of the commands for master and slave.
2020-09-21 18:00:07 +02:00
Hornwitser bf5a4a8a9c Split up into multiple packages with Lerna
Split up codebase into the packages @clsterio/lib, @clusterio/master,
@clusterio/slave, and @clusterio/ctl using lerna to manage them.  This
will allow deployment to use only the parts needed reducing install
size, and better separate the code into shared library, master, slave
and control.
2020-09-21 18:00:07 +02:00
Hornwitser 198d5c344b Remove unused dependencies from dead code
Remove dependencies used by inventoryImports, external plugins and the
removed modManager code.
2020-09-16 15:09:23 +02:00
Hornwitser efa3eff0d5 Delete old mod manager code 2020-09-16 15:09:23 +02:00
Hornwitser 6fde461da5 Merge pull request #313 from Hornwitser/list-management
List management
2020-09-16 15:08:21 +02:00
dependabot[bot] 14ddb76a0b Bump node-forge from 0.7.6 to 0.10.0 (#315)
Bumps [node-forge](https://github.com/digitalbazaar/forge) from 0.7.6 to 0.10.0.
- [Release notes](https://github.com/digitalbazaar/forge/releases)
- [Changelog](https://github.com/digitalbazaar/forge/blob/master/CHANGELOG.md)
- [Commits](https://github.com/digitalbazaar/forge/compare/0.7.6...0.10.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-09-16 14:59:35 +02:00
Hornwitser 3753c303dd Add changelog entry for list synchronization
[skip travis]
2020-09-01 20:33:31 +02:00
Hornwitser 08ec542907 Move integration tests for lists sync
Move the integration tests for the admin, user, and ban list sync to be
done while an instance is running in order to hit more lines in the
coverage with them.
2020-09-01 20:16:17 +02:00
Hornwitser a8b8232d73 Merge pull request #314 from RMuskovets/readme-typo-factorio-version
Fix a typo in README.md
2020-09-01 19:17:56 +02:00
RMuskovets 4f86be1b7f Fix a typo in README.md:
Line 174, "Factorio_0.x.y" -> "Factorio.x.y.z"
Since 1.0.0 release Factorio versions no longer start from 0
2020-09-01 19:56:08 +03:00
Hornwitser 84450b83b4 Implement adminlist, whitelist and banlist sync
Add integration of the server adminlist, whitelist and banlist into
clusterio.  Membership is stored as booleans in the user acconuts and
changes are synchronized live to slaves which pass it on to instances
which in turn will update the running server's lists with RCON commands.
2020-09-01 18:17:40 +02:00
Hornwitser f08774bd4a Add execCtl shortcut for clusterctl tests
Add execCtl which is a shortcut to execute node clusterctl with the test
config set.
2020-09-01 18:17:40 +02:00
Hornwitser 5e5a47ce02 Leave out roles array in serialized user
Leave out the roles property containing the array of role ids if the
user does not have any roles.
2020-09-01 17:59:19 +02:00
Hornwitser 9761fbb71c Fix test names for clusterctl
Rename the test cases named after the old clusterctl commands for the
integrations to the new clusterctl commands.
2020-09-01 17:59:19 +02:00
Hornwitser 3c55188a87 Merge pull request #311 from Hornwitser/fix-logger-warning
Fix logger.warning is not a function

Fixes #303
2020-08-27 11:41:43 +02:00
Hornwitser a40c78d85d Fix logger.warning is not a function
Replace logger.warning with logger.warn as I incorrectly assumed the
warning method of console was called console.warning instead of
console.warn.
2020-08-27 11:31:36 +02:00
Arvids Godjuks 0b790357f9 Merge pull request #310 from Hornwitser/remove-bcrypt
Remove bcrypt dependency
2020-08-27 11:28:11 +02:00
Hornwitser 2dcc5c9d47 Remove bcrypt dependency
Remove the unused dependencies to bcrypt and bcrypt-promise.  These were
used by the playerManager plugin to implement password based
authentication, but this plugin is likely not going to be ported to 2.0.
2020-08-27 11:17:35 +02:00
dependabot[bot] 6d8d926268 Bump express-fileupload from 1.1.5 to 1.2.0 (#308)
Bumps [express-fileupload](https://github.com/richardgirges/express-fileupload) from 1.1.5 to 1.2.0.
- [Release notes](https://github.com/richardgirges/express-fileupload/releases)
- [Commits](https://github.com/richardgirges/express-fileupload/compare/v1.1.5...1.2.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-08-27 11:07:35 +02:00
Hornwitser c59c30b840 Merge pull request #304 from Hornwitser/clusterctl-restructure
Restructre clusterctl cli interface
2020-08-26 13:23:33 +02:00
Hornwitser fd307e58c2 Add global-chat shout command
Add command to broadcast a message to all instances from clusterctl.
2020-08-18 18:52:11 +02:00
Hornwitser da1cbc16f7 Restructre clusterctl cli interface
Turn the flat list of commands into a hierarchy of commands, converting
most required parameters into positional parameters and add plugin
loading support for clusterctl.
2020-08-18 18:52:11 +02:00
ToxicFrazzles 9b870fde64 Fix syntax error in plugins/subspace_storage/static/storage.js (#302)
Fix _lastData being used as a global causing the script to fail to load
after strict mode was added.
2020-07-08 12:51:16 +02:00
Hornwitser 0802c5b29d Merge pull request #301 from Hornwitser/custom-scenario
Add support for running custom scenarios
2020-07-08 12:46:49 +02:00
Hornwitser 781e40428f Add enable_save_patching to mock instance config
Handle the factorio.enable_save_patching config option in the mock
instance as these are queried by instances after 9458155.
2020-07-07 14:24:47 +02:00
Hornwitser 999386c088 Add --keep-open option to clusterctl
Add option to keep the console log open after starting an instance
instead of exiting as soon as it has finished starting up.
2020-07-06 13:16:57 +02:00
Hornwitser 8c6130adba Add load scenario command
Add alternative method of starting an instance by loading a scenario.
Since there's no save when starting a scenario it can only be used if
the save patching is disabled.
2020-07-06 13:10:26 +02:00
Hornwitser 9458155a1b Add option to disable save patching
Add factorio.enable_save_patching config entry to allow disabling the
save patching done on startup.  This is for running vannila games or
scenarios that are not compatible with Clusterio.
2020-07-04 16:45:42 +02:00
Hornwitser 0969e07d79 Merge pull request #300 from Hornwitser/lint
Lint codebase
2020-06-17 22:12:27 +02:00
Hornwitser 9c9b3ac625 Document running tests and linting
Add section to contributing docs detailing how to run tests and linting
on the project.
2020-06-17 21:08:45 +02:00
Hornwitser 7814656241 Add ESLint check to travis integration
Invoke the lint package.json script during travis integration check.
2020-06-17 20:59:37 +02:00
Hornwitser e9f5cae4f0 Fix misc problems detected by ESLint
Reformat and clean up issues detected by ESLint.
2020-06-17 20:59:37 +02:00
Hornwitser 8cb21c5fa8 Add dangling commas
Ensure multiline object and array litterals are consistenly typed out
with dangling commas for the last element.
2020-06-17 20:59:37 +02:00
Hornwitser 3e4f0b6b5e Fix indents
Fix incorrectly indented code as detected by ESLint.
2020-06-17 20:59:33 +02:00
Hornwitser 703fda43ad Fix JSDoc issues
Fix missing documentation for parameters and return types, incorect
config and errors in type definitions.
2020-06-17 20:53:18 +02:00
Hornwitser 3dba92a03d Use strict mode
Add strict mode directives to all js files.
2020-06-17 20:53:18 +02:00
Hornwitser b254e4189d Use more consistent spacing in objects literals 2020-06-17 20:53:18 +02:00
Hornwitser 29f066a191 Fix missing semicolons
Ensure all statements end with a semicolon.
2020-06-17 20:53:18 +02:00
Hornwitser 34aaeee354 Use double quotes
Convert existing mix of double quotes and single quotes to only use
double quotes unless the string contains double quotes.
2020-06-17 20:53:18 +02:00
Hornwitser efd759bec5 Add code linting using ESLint
Add ESLint as a dev dependency and configure it to match the preferred
style for the project.  The config options set to warn are tentative and
may not be sensible as they haven't been fully investigated yet.
2020-06-17 20:53:18 +02:00
Hornwitser 922c6af632 Merge pull request #299 from Hornwitser/user-management
User management and authentication rework
2020-06-17 20:47:52 +02:00
Hornwitser cf6f2cb70a Add user and permission system
Add users, roles and permissions and rework the authentication to use
separate user and slave tokens for control and slave connections
respectively.

Add commands for listing permissions, roles and users, and creating,
editing and deleting roles and users.
2020-06-15 16:47:15 +02:00
Hornwitser 2acf57d23d Fix erroneous delete of local variable
The delete operator only works on object properties, and causes the code
coverage to fail for the file in question if used on a local variable.
2020-06-15 00:13:25 +02:00
Hornwitser 07cff0ab9a Fix error during slave authentication failure
Treat sending messages over a link that is not connected as a
SessionLost error and handle AuthenticationFailed error in the
startup logic of slave.
2020-06-15 00:13:25 +02:00
Hornwitser 3dfed4d2ad Remove master.factorio_directory config
Remove old config entry for the old locale API that was removed with the
introduction of the new export system in 260f976.
2020-06-15 00:13:25 +02:00
Hornwitser c3179f0423 Move ControlConfig to lib/config/definitions.js
Move the definition of the control config to lib/config so that the
master code can use it.
2020-06-15 00:13:25 +02:00
Hornwitser 37bff10382 Merge pull request #298 from Hornwitser/config-event
Config events
2020-06-11 22:07:23 +02:00
Hornwitser 931ad4816f Update config of running Factorio instances
Monitor the factorio.settings config for changes and apply them to
running instances via the /config set command.
2020-05-31 22:28:51 +02:00
Hornwitser 5b043524b4 Implement setProp in configs
Add setProp method for selectively changing one property of an object
field in a config group.
2020-05-31 16:08:21 +02:00
Hornwitser 8c9daff0bb Add name shortcut for ConfigGroup instances
Add .name shortcut to get the groupName class property of config group
instances.
2020-05-31 14:16:06 +02:00
Hornwitser 67ca8dcc4d Implement config notification event
Add fieldChanged event to config objects that is triggered when the
value of a field in a group belonging to it is changed.
2020-05-30 14:27:59 +02:00
Hornwitser fc3686b865 Invoke plugin hooks via common function
Use a common function to iterate through and invoke the given hook in
every plugin, and ignore errors thrown by plugins from these hooks.
2020-05-30 14:27:59 +02:00
Hornwitser 01ea301a8c Merge pull request #297 from Hornwitser/automate-export
Automate export process
2020-05-26 18:13:17 +02:00
Hornwitser 36cf14074e Automatically upload export to master server
Upload the created export data zip file to a new /api/upload-export
endpoint on the master server which unpacks it into the appropriate
directory on the master.
2020-05-26 15:31:42 +02:00
Hornwitser f3d4cfdcf5 Add reference to Slave in Instance
Add a reference to the Slave that's running the given instance in order
for it to be able to access the slave config.
2020-05-26 14:04:25 +02:00
Hornwitser f8597f500c Remove license note for angels icons
Remove notice about license for the now removed modded angels icons.
2020-05-26 13:17:58 +02:00
Hornwitser 2b20c943bb Merge pull request #296 from Hornwitser/item-export
Implement icon and locale export

Closes #290
2020-05-26 13:16:11 +02:00
Hornwitser 1bddea8cec Remove Factorio icons
Remove no longer used Factorio icons.
2020-05-26 01:12:05 +02:00
Hornwitser e2255ddb7c Remove inclusion of 3625 Angels item icons
Remove obsolete inclusion of the 3625 Angels item icons added by
09f476e.
2020-05-26 01:12:05 +02:00
Hornwitser 453226f246 Implement locale trenslation of resource names
Translate the names of the resources using the exported Factorio locale.
2020-05-26 01:12:05 +02:00
Hornwitser 260f976fa3 Implement export of item icons and locale
Add mod and command for exporting the Factorio locale including mods,
and item icons assemled into a sprite sheet including mods.
2020-05-26 01:11:49 +02:00
Hornwitser d1b851d890 Move clusterio_lib to lua/clusterio_lib
Move the library mod to lua/clusterio_lib and restructure build_mod.js
to handle additonal files as a property to info.js instead of being hard
coded.
2020-05-25 20:45:09 +02:00
Hornwitser 2e24d6c8ae Merge pull request #295 from Hornwitser/fix-windows
Fix Windows stop logic
2020-05-25 17:58:54 +02:00
Hornwitser 203f2d2a14 Fix Windows stop logic
Handle the Factorio server not exiting when encountering an error on
Windows and fix the hang detection for .stop().
2020-05-21 18:06:20 +02:00
Hornwitser 87d14252fc Merge pull request #294 from Hornwitser/plugin-track
Integrate plugins closer with connections
2020-05-12 14:23:04 +02:00
Hornwitser 3211fbe985 Notify master about instance status update
Send updates to the master server when the running status of instances
change and add onInstanceStatusChanged hook to master plugins.
2020-05-11 18:10:11 +02:00
Hornwitser a8e29fdc56 Separate instance from config in master
Separate the instance object and the config that belongs to the instance
object in the instances db held in memory on the master.
2020-05-11 18:07:41 +02:00
Hornwitser a194508f52 Filter plugin messages forwarded to links
Drop events forwarded to a slave or instance that does not have the
plugin the event is defined in.  Reject requests for the same reason.
2020-05-11 18:04:03 +02:00
Hornwitser ca2839594e Timeout quikly when disconnecting existing slave
If a slave that is already connected attempts connecting the existing
connection for that slave is disconnected.  The most common scenario for
this is that the existing connection is stale and the slave is
attempting to connect agan.  However if the connection is stale then the
disconnection will take 15 minutes to time out and the slave will appear
unresponsive during this time.  Fix this problem by setting the timeout
to 15 seconds before disconnecting the existing connection.
2020-05-09 16:36:33 +02:00
Hornwitser 1b427af5f0 Fix client send buffer not being sent on ready
Fix WebSocketClientConnector not sending the send buffer queued up
between starting the connection and the master server sending the ready
signal for it.
2020-05-09 16:34:46 +02:00
Hornwitser 04ee3d3f96 Fix WebSocketClientConnector lifecycle events
Fix drop being repeatedly emitted while reconnecting and close not being
emitted on connection timeout.
2020-05-09 16:28:07 +02:00
Hornwitser c87538ca58 Handle connection being closed in slave
Listen for the close event and connect again if the connection was
closed from proper disconnect or initiate a shutdown if the connection
was unexpectedly closed.  Unexpected closure happens from authentication
failure and if a connection fails to reconnect before the timeout.
2020-05-09 16:24:02 +02:00
Hornwitser c35b6de70e Notify on connection events
Implement notification to plugins for disconnect request, connect, drop,
and close.  This allows plugins to properly handle slaves and the master
server shutting down without sending messages that will get lost.
2020-05-08 23:49:28 +02:00
Hornwitser b8b8c44083 Fix onExit not being invoked if plugin load fails
Fix instance not properly exiting if an error occurs during the loading
of a plugin.
2020-05-08 18:15:17 +02:00
Hornwitser 1beffb1bfd Fix crash on hartbeat on closing connection
Fix heartbeat incorrectly asserting it cannot be sent on a connection in
the closing state.
2020-05-08 18:09:48 +02:00
Hornwitser b151dd8469 Fix documentation
Fix missing module declarations, broken lings, broken formating and
incorrect scopes in the documentation.  For modules split over several
files it appears the only viable option is to use the memberof directive
on each documented member and only declare the index file as a module.
2020-05-08 17:28:06 +02:00
Hornwitser 7a9b6592e6 Handle disconnect in link integration test
Handle the prepareDisconnect request properly in the TestControl in
test/integration/link.js.
2020-05-07 13:23:11 +02:00
Hornwitser 5c1214ad38 Rename connected and dropped events
Rename the connector events connected and dropped to connect and drop
respectively to be consistent with common Node.js names for events like
close.
2020-05-07 12:36:43 +02:00
Hornwitser 1629eaf225 Track plugins loaded by master and slaves
Send a mapping of the plugins the master server has loaded to slaves
vice versa.  Limit the plugins that may be loaded by instances to only
those that the master server has loaded.
2020-05-06 20:14:40 +02:00
Hornwitser 4060c75142 Merge pull request #293 from Hornwitser/alpha-bugfixes-2
Alpha bugfixes 2
2020-05-06 20:13:22 +02:00
Hornwitser f3c13eab21 Simplify level logic
Simplify tech level logic according to TheAiBot's suggestion.
2020-05-06 19:49:33 +02:00
Hornwitser e9fab3cc10 Fix level > max_level crash in research_sync
Fix crash when technologiy level stored on the master is greater than
the max_level for the technology in Factorio.  This can happen with mods
that for example reduce the max_level from inifity to a finite level.
2020-04-24 15:27:30 +02:00
Hornwitser 446333f4aa Fix stylesheet loaded from wrong location
Fix stylesheet path not taking into account the web_root config.
2020-04-24 15:26:52 +02:00
Hornwitser f8817ecf0c Merge pull request #292 from Hornwitser/alpha-bugfixes
Alpha bugfixes
2020-04-19 23:32:10 +02:00
Hornwitser c601c53044 Fix typo in clusterctl.js
Fix SessionLost being refered to as a global instead of from the errors
module in clusterctl.js.
2020-04-19 18:24:43 +02:00
Hornwitser 3603b16901 Fix missing space in researched notification
Add space between the technology name and the level in the notification
for researched technologies.
2020-04-19 18:23:37 +02:00
Hornwitser f4ca8366e2 Handle contributions to the next level of a technology
Fix research_sync sending out contribution events for the previous level
of a researched technology and sending finished events for the wrong
level when synchronizing technologies.  Both of these issues are related
to failing to accunt for the unlocked level being one less than the
level stored when the researched property is false.
2020-04-19 18:20:28 +02:00
Hornwitser b6585e6364 Fix reconnect giving up too early
Fix the reconnect logic giving immediately if a previous reconnection
had been made long prior to the connection being dropped again.
2020-04-18 23:37:50 +02:00
Hornwitser d2fb33f8b9 Fix log spam by subspace_storage plugin
Fix log spam when the dole distribution method is used on
subspace_storage plugin.  This was caused by a bogus if condition being
translated into a sensible but wrong one.
2020-04-18 23:37:50 +02:00
MyNameIsTrez 568cf81204 Edit docs/writing-plugins.md (#287)
Fix grammar, punctuation and spelling mistakes in
docs/writing-plugins.md.
2020-04-18 23:34:26 +02:00
Hornwitser 15ad64e356 Fix web icons ignoring web_root (#291)
Fix the URLs for the item icons in the web interface ignoring the
web_root config option.

[travis skip]
2020-04-17 17:46:17 +02:00
MyNameIsTrez ef6441ca46 Edit docs/developing-for-clusterio.md (#285)
Fix grammar, punctuation and spelling mistakes in
docs/developing-for-clusterio.md.
2020-04-17 15:46:35 +02:00
Hornwitser 534408856c Merge pull request #284 from Hornwitser/small-things-2
Small things 2
2020-04-11 02:04:59 +02:00
Hornwitser 430aa6d207 Disable heap and memory size tests
Disable the tests for the memory and heap size metrics as the travis
environment is apparently using 3x as much memory as locally.
2020-04-09 20:44:06 +02:00
Hornwitser 00d1d304d2 Fix integration test certificate path
Fix integration tests using the default certificate paths instead of
having them located in the dedicated test database directory.  Also
increment key size to the minimum allowed of 1024.
2020-04-09 20:35:51 +02:00
Hornwitser 38c392ff6d Fix kill logic of integration test
Test if the process variable exists before trying to call kill on it, as
in certain failure situations it is not set.
2020-04-09 20:08:04 +02:00
Hornwitser 4f463a8180 Reduce number of bits in generated test cert
Add config entry for the number of bits to use in the auto generate TLS
certificate for the master server and reduce it to 512 for the
integration test.  This reduces the time needed to generate the
certificate in the integration test.
2020-04-09 20:02:29 +02:00
Hornwitser 2c855a5846 Add debug-dump-ws command
Add debug command to dump the WebSocket messages being sent and received
by the master server to aid in finding chatty events.
2020-04-09 19:50:01 +02:00
Hornwitser 61d92a6910 Rename master plugin onExit hook to onShutdown
Rename the onExit hook for master plugins to onShutdown to better
reflect when it is invoked.
2020-04-09 19:50:01 +02:00
Hornwitser 9507608e59 Rework disconnect logic for connectors
Rework the way disconnection at shutdown is done to properly close
the connections when the master server is shut down.
2020-04-09 19:50:01 +02:00
Hornwitser 47ea8f5477 Rename connection shutdown to prepare disconnect
Rename the shutdownConnection request to prepareDisconnect to better
reflect its actual purpose.
2020-04-09 16:17:38 +02:00
Hornwitser e7d1039942 Link to writings plugins document in docs
Replace notice about in development plugin interface with a link to the
writing plugins document in the developing for clusterio document.
2020-04-09 16:17:38 +02:00
Hornwitser fc04fd9ebc Remove master branch warning
Remove warning of master branch being broken now that it's no longer
completely broken.
2020-04-09 16:17:38 +02:00
Hornwitser 3dc4bdaab6 Remove debug log
Remove debug log statement annoing events have been broadcast.
2020-04-09 16:17:38 +02:00
Hornwitser 59111cc844 Disable replay in patched save
Ensure that the replay is disabled after the game is patched by calling
game.disable_replay().  It's not possible to replay a patched save, and
it also stores every command sent into save, causing it to quickly grow
into an unplayably large size.
2020-04-09 16:17:38 +02:00
Hornwitser d537e5cf6b Fix typo in default cpu metric
Fix proces_cpu_seconds_total being mislabled as process_cpu_seconds.
2020-04-09 16:17:38 +02:00
Hornwitser 47c221f6f3 Fix typo in test names
Fix the labels function being refered to as the label function in the
test descriptions.
2020-04-09 16:17:38 +02:00
Hornwitser 56488b2c2d Add resident memory and heap size metrics
Implement the default resident memory and heap size metrics.
2020-04-09 16:17:38 +02:00
Hornwitser e908c5069b Add autosave tracking and statistics
Detect when autosaves are made and track the size in a Prometheus gague.
2020-04-09 16:17:38 +02:00
Hornwitser 139585f8c6 Prevent metrics being called on createSave
Gate the onMetrics plugin hook behind a boolean that's set only after
the Factorio server has fully started, and reset when it stops.  This
prevents errors due to plugins trying to get metrics while the server
is creating a new save or stopped.
2020-04-09 16:17:38 +02:00
Hornwitser 6bc07ad1b3 Add Factorio CPU and Memory usage statistics
Add per instance metrics for CPU and memory usage with the pidusage
library.
2020-04-09 16:17:38 +02:00
Hornwitser 56ee1fe955 Rework save logic with regard to start instance
Ensure that notifyExit is called if errors occur during startInstance
and createSave request, and rework startInstance save logic to create
a new savegame if no save is present and allow specifying the save to
load.
2020-04-09 16:17:38 +02:00
Hornwitser 62e38199bd Rework exit notification for Instances
Pull the exit handler into its own method that's invoked if the startup
fails before the Factorio server is created.  Also move the metrics
clearing to the exit notification method to ensure it's also invoked.
2020-04-09 16:17:38 +02:00
Hornwitser 407e3d2aed Merge pull request #283 from clusterio/TheAIBot-patch-1
Fix overflow check in subspace storage
2020-04-08 15:08:32 +02:00
TheAIBot fcfad08260 fix overflow check in subspace storage
Previous and new value:
0x7ffffff -- old 134.217.727
0x7fffffff -- new 2.147.483.647
2020-04-08 14:59:53 +02:00
Hornwitser 112cad8a49 Merge pull request #282 from Hornwitser/small-things
Small things
2020-04-06 14:50:55 +02:00
Hornwitser ccb07d6eab Add support for Factorio 0.18.x
Add option to bump patch number and override factorio_version in the
generated mod and update the serialize library to support the inventory
bar access methods of both 0.17 and 0.18.
2020-04-06 14:41:51 +02:00
Hornwitser 621c834126 Merge pull request #281 from Hornwitser/reconnection-logic
Reconnection logic
2020-04-04 18:39:36 +02:00
Hornwitser 75cd6d38db Fix instance exit notification
Fix improper notification on Factorio server exit causing plugins and
the instance tracking to fail.
2020-04-04 15:14:23 +02:00
Hornwitser 99ac8f4f33 Add commands sent statistic
Track RCON commands sent.
2020-04-04 12:18:26 +02:00
Hornwitser 75067e992d Add WebSocket usage statistics
Add Prometheus metrics for messages sent, connections made, connections
rejected, active connections, and active slaves.
2020-04-04 12:16:57 +02:00
Hornwitser 31b66339a0 Remove Google Analytics
Remove hard coded Google Analytics integration in web interface.
2020-04-04 10:15:27 +02:00
Hornwitser 8ebf0fc754 Add master server Web Root config
Add web_root option to the master server and use it create URLs for
resources hosted by it.  This permits hosting the master server web
interface behind reverse proxy under a dedicated path.
2020-04-04 10:05:37 +02:00
Hornwitser 5e9d9097e1 Link mods when creating save
Ensure the mods are linked in when creating a new save.
2020-04-03 22:32:12 +02:00
Hornwitser 3343d53d75 Allow deleting unassigned instances
Handle delete instance requests for instances that are not assigned to a
slave.
2020-04-03 22:22:23 +02:00
Hornwitser 541d2ad337 Add missing changlog entry for 737c9924
Add entry to changelog noting that the clusterioMod plugin has been
renamed to subspace_storage.
2020-04-03 21:15:57 +02:00
Hornwitser 59758f1576 Rename plugin Prometheus metrics
Rename Prometheus metrics exported by plugins to follow the
clusterio_<plugin_name>_<metric_name> naming scheme.
2020-04-03 21:14:30 +02:00
Hornwitser 706ffd9ed2 Detail usage of single and double quoted strings
Add section to contributing guidelines detailing that single quoted
strings should not be used.  The use of it has only resulted in the
addition of more inconsistency to the codebase.
2020-04-03 20:51:28 +02:00
Hornwitser 17973e767e Generate API docs with JSDoc
Add npm docs script to generate api docs with JSDoc.
2020-04-03 20:51:28 +02:00
Hornwitser 5c16809b5b JSDoc use autodetected module name
Change @module <module name> entries to just @module as JSDoc is able to
automatically determine the module name.
2020-04-03 20:51:28 +02:00
Hornwitser ee063300ed Check result of rcon commands in subspace_storage
Validate that the commands sent don't error out by checking the returned
result is empty.
2020-04-03 20:51:28 +02:00
Hornwitser 3cc84d6400 Use basicType helper in lib/database
Simplify the checking logic in lib/database by using the basicType
helper.
2020-04-03 20:51:28 +02:00
Hornwitser 65494a0a91 Move basicType to lib/helpers
Move the basicType function from the config lib to a shared helpers lib.
2020-04-03 20:51:28 +02:00
Hornwitser f05100c94d Remove unused files
Remove left over files that are no longer in use.
2020-04-03 20:51:28 +02:00
Hornwitser 9a4a535add Expose Slave to instance plugins
Allow the slave object to be accessed by instance plugins through the
this.slave property.
2020-04-03 20:51:28 +02:00
Hornwitser 3ff5e630ca Warn on unhandled Factorio ipc
Send warning to console if an ipc message is received but there were no
handlers registered to handle it.
2020-04-03 20:51:28 +02:00
Hornwitser c2df80a17f Move test/json to test/file/json
Move the json test files for the lib/database tests to test/file/json to
be consistent with the location of test files for other tests.
2020-04-03 20:51:28 +02:00
Hornwitser f9f0e5b5f7 Move test/temp to temp/test
Move the temporary files generated by the the tests to the top level
directory temp.
2020-04-03 20:51:28 +02:00
Hornwitser 028f00e58a Rename client.js to slave.js
Rename the client.js script to slave.js, and changed places where it's
refered to as client into slave.
2020-04-03 20:51:28 +02:00
Hornwitser ae86781b0d Add support for multiple Factorio installs
Allow the factorio_directory specified in the slave config to contain
multiple Factorio installs and add factior.version instance option to
select which install to use for the given instance.
2020-04-03 20:51:28 +02:00
Hornwitser 4c26fcee3a Fix connection shutdown logic
Fix the WebSocket link not being properly teared down on the master
side causing the handshake to deadlock when the slave tried to connect
again.
2020-04-03 20:44:20 +02:00
Hornwitser d4e072f376 Rewrite reconnection logic
Implement proper session tracking and resume on reconnect for the
WebSocket server.
2020-04-02 21:58:44 +02:00
Hornwitser 026b7f1306 Fix missing onExit calls
Add missing calls to instance plugin onExit hooks.
2020-04-02 19:56:39 +02:00
Hornwitser 8de3f54bc4 Move slowTest into test/integration/index
Deduplicate slowTest into the shared integration test library.
2020-04-02 15:24:32 +02:00
Hornwitser 43ce6aa58c Pass validation errors with InvalidMessage error
Pass the array describing the validation errors occuring with the
InvalidMessage error throw and print it to the console.
2020-04-02 14:39:18 +02:00
Hornwitser 0a9fff7578 Merge pull request #280 from Hornwitser/brand-as-alpha
Brand library mod as alpha
2020-03-23 21:22:41 +01:00
Hornwitser b73f2f6006 Brand library mod as alpha
Add alpha notice to the title and description for the Clusterio Library
Factorio mod.
2020-03-23 21:11:34 +01:00
Hornwitser 277b78bf6d Merge pull request #279 from Hornwitser/ditch-socket-io
Ditch socket.io
2020-03-23 02:49:31 +01:00
Hornwitser 1aef3c9609 Do not use shell for spawned integration nodes
When using the shell for spawning the master and slave integration
processes the shell becomes the process that is returned by the spawn
command.  This in turn means that the SIGINT that was supposed to be
sent to the master and slave processes got ignored by the shell instead.

Fix by spawning the node process directly instead of through a shell.
2020-03-23 02:38:40 +01:00
Hornwitser 9f74d7b2ec Fix shutdown logic for slave
Shutdown running instances and close the connection to the master server
on slave stop.  Remove call to process.exit on shutdown as it's no
longer needed.
2020-03-22 03:30:57 +01:00
Hornwitser 7aa340675b Gracefully shut down integration master and slave
Use SIGINT instead of the default SIGTERM when signalling to shut down
the integration master and slave subprocesses.  This should let nyc
properly write the coverage report before it exits.
2020-03-22 02:39:20 +01:00
Hornwitser 27ef00705b Set content type for /api/inventory
Set the correct content type to JSON to for the /api/inventory endpoints.
2020-03-22 02:10:54 +01:00
Hornwitser 38c60c221f Use sub-processes in integration testing
Spawn master, slave, and clusterctl in separate processes in the
clusterio integration tests and leave the test master and slave process
available for the whole test duration to allow plugins to use them as
well.  This removes the need for reimplementing the startup logic in the
test code and makes the existing startup code covered by tests.
2020-03-22 02:10:54 +01:00
Hornwitser 677512512e Replace socket.io with plain WebSocket
Replace socket.io with plain WebSocket using the ws library as it
doesn't really provide any useful tools over plain WebSocket when it
comes to implementing reliable communication that works across
reconnects.  The widespread browser support for WebSocket also means
it has little value to add for the web interface over WebSocket.
2020-03-22 02:10:54 +01:00
Hornwitser 808bd6b453 Validate messages sent
Validate messages sent over links with the json schema provided from
them.  This aids debugging as an exception will be thrown from the code
sending the bad content.
2020-03-22 02:10:54 +01:00
Hornwitser 941ceab935 Merge pull request #278 from Hornwitser/plugin-rewrite
Plugin rewrite and code improvements
2020-03-21 19:38:46 +01:00
Hornwitser 9dac57b36b Port Research Sync plugin
Rewriwe the researchSync plugin into plugins/research_sync.
2020-03-17 01:31:51 +01:00
Hornwitser 23be732a5c Merge pull request #277 from Hornwitser/config-rewrite
Initial config system rewrite
2020-03-17 00:52:49 +01:00
Hornwitser 737c9924b9 Port and rename clusterioMod plugin
Port the clusterioMod plugin to the new plugin system under the new
subspace_storage name.
2020-03-13 22:46:17 +01:00
Hornwitser d9fe408c42 Fix master web interface
Fix the breaking changes to the plugin struture and remove slave
information that is no longer present.
2020-03-13 22:46:17 +01:00
Hornwitser 090fbd6dff Expose master interfaces to plugin
Expose Express.js app, master config, instance and slave database, slave
connections and metrics to the master plugins.
2020-03-13 22:46:17 +01:00
Hornwitser cb1d744060 Handle errors during master plugin initialization
Catch errors thrown when constructing or initializing master pluigns and
wrap them in PluginError to ensure that they are properly reported as
plugin errors.
2020-03-13 22:46:17 +01:00
Hornwitser ddd2027739 Handle errors during instance startup
Catch errors and shut down the instance in question if the startup
failed.  This happens if for example the Factorio server errors out on a
script during startup.
2020-03-13 22:46:17 +01:00
Hornwitser 6a8346186f Implemented forwarding of requests to the master
Implement the forwardTo function for requests to the master server.
2020-03-13 22:46:17 +01:00
Hornwitser 5681ef9733 Fix plugin enabled config being inverted
Fix plugins being loaded when enable was set to false, and not loaded
when set to true.
2020-03-13 22:46:17 +01:00
Hornwitser 6f22942cc5 Fix incorrect instance lookups in plugins
Fix mistaken lookups missing the instance part, or trying to get the id
from the removed instance.id property.
2020-03-13 22:46:17 +01:00
Hornwitser 1e6b807ae3 Add Clusterio Library mod
Add a library mod for wrapping the Clusterio remote API and providing
related utilities.  This eases development of Clusterio related Lua code
by providing the same interface for both mods and modules and also makes
the serialize library available without having to duplicate it.

It is not possible to implement this through save patching as mods
cannot load Lua code from the level code like they can with code from
other mods.
2020-03-12 15:39:25 +01:00
Hornwitser 5ef6d02054 Add serialize library to clusterio module
Add library for serializing items and inventories to the clusterio
module.  This will provide a common codebase for plugins like
playerManager and trainTeleports to use in order to serialize
inventories to the master server.
2020-03-10 10:57:24 +01:00
Hornwitser eac17b4340 Remove reference to pm2
Remove the instructions for installing and using pm2 with clusterio, as
I don't know how to use this tool properly and can't keep the README up
to date on it.
2020-03-09 20:53:01 +01:00
Hornwitser 54224755e9 Add missing chalk dependency
Add chalk as a dependency as it's used in clusterctl.js.
2020-03-09 20:50:58 +01:00
Hornwitser 557d380112 Fix broken link to modules/clusterio/api.lua
Fix broken link to in docs/developing-for-clusterio.md to
modules/clusterio/api.lua.
2020-03-09 20:48:21 +01:00
Hornwitser 5e5149dc93 Implement new configuration system
Replace current configuration mess with a trimmed down version of the
original vision of #200.  Configs are collection of config groups that
have fields that can be set.  These groups allow organizing the fields
as well as opertunity for plugins to add their own groups.  By default
plugins have an auto generated group with the enabled field.
2020-03-09 20:48:13 +01:00
Hornwitser 370ee9ed04 Unminify formatBytes
There's no reason to minify formatBytes it saves nothing.  Use the
unminified version as published.
2020-01-14 08:09:39 +01:00
Hornwitser f4a0b88ff6 Remove remoteCommands plugin config
Remove the remoteCommand specific config allowRemoteCommandExecution.
The plugin has been removed and with the move to master managing the
instances/slaves it makes no sense to deny remote commands.
2020-01-14 08:09:39 +01:00
Hornwitser e137547664 Remove obsolete msBetweenCommands config
Remove the no longer used msBetweenCommands config.  The RCON connection
was changed to be limited to 5 concurrent commands instead.
2020-01-14 08:09:39 +01:00
Hornwitser d5c43cc3d6 Remove clusterioMod specific configs
Remove configuration options in the master config that's specific to the
clusterioMod plugin.
2020-01-14 08:09:39 +01:00
Hornwitser 459c039f17 Merge pull request #276 from Hornwitser/module-rework
Module rework
2020-01-07 16:16:25 +01:00
Hornwitser 892c803496 Add on_server_startup event
Add startup event to the Clusterio module API which is raised at startup
of a Factorio server.  This is to server as a replacement for the
on_init and on_configuration_changed events which cannot be used in
modules.

Adds clusterio_patch_number global from the patcher in order to
implement this.  This is a number that's incremented every time the save
is patched.
2020-01-05 23:19:34 +01:00
Hornwitser b8fb73f863 Add Factorio to Clusterio communication API
Add send_json to the Clusterio module's API which allows sending
arbitrary tables serialized as JSON to plugins that listen on the
channel specified.
2020-01-04 17:34:16 +01:00
Hornwitser 6cf16cc09e Reference by global in statistics_exporter
Store export function in a global table in the lua module for statistics
exporter in order to avoid a remote.call.
2020-01-04 17:34:16 +01:00
Hornwitser 0e9cd35c30 Add table of contents to Writing Plugins
Add table of contents for the Writing plugins document.
2020-01-04 17:34:16 +01:00
Hornwitser 66aa7e1e43 Add clusterio module with API for lua code
Add a shared API for lua code through a module named clusterio.
2020-01-04 17:34:16 +01:00
Hornwitser 221dc855f4 Add option to throw on non-empty RCON responses
Add option to FactorioServer.sendRcon to throw on non-empty responses
from Factorio.  This is useful for detecting errors returned by commands
that should not have a response.
2020-01-04 17:34:16 +01:00
Hornwitser f88e1f1d3a Remove dead code left over from 21d9fdf
Remove compatConfig definition after RCON is established which was left
over from 21d9fdf.
2020-01-04 17:34:16 +01:00
Hornwitser c091dc341e Implement require option for modules
Implement option allowing files to be required from control.lua be
specified in modules.json.
2020-01-04 17:34:16 +01:00
Hornwitser e89e54ee27 Implement dependencies specification for modules
Implement dependency ordering and version checking for modules.
2020-01-04 17:34:16 +01:00
Hornwitser 80bd75ba16 Rework module loading
Change module loading to use a module.json file describing the module
and patch in all files in a directory path passed to the patcher instead
of having to pass the files names and their content.  This allows the
specification of which files should be passed to the event_handler lib
and an easy implementation of loading stand alone modules.
2020-01-04 17:34:16 +01:00
Hornwitser 11992d1839 Merge pull request #275 from Hornwitser/statistics-fixes
Statistics plugin test and minor fix
2020-01-04 17:33:48 +01:00
Hornwitser 610859eb3b Improve error message from statistics_exporter
If the JSON parse failed it will usually be because Factorio returned an
error message instead of JSON, pass this along for easier debugging.
2019-12-29 04:44:16 +01:00
Hornwitser 37d8eedc0d Add tests for statistics_exporter plugin
These were already made at the time I ported the plugin but somehow I
forgot to add them to the statistics rework commit (26eefb0) :/.
2019-12-29 04:42:13 +01:00
Hornwitser 9d2e89ec63 Merge pull request #274 from Hornwitser/document-version-requirement
Document version requirement
2019-12-29 04:25:32 +01:00
Hornwitser 1b61d09a07 Add docs section on supported versions
Add section in the contributing docs about which version of Node.js and
Factorio is supported.

[skip travis]
2019-12-27 06:26:22 +01:00
Arvids Godjuks ac3aa5ffee Merge pull request #271 from Hornwitser/remove-old-stats
Remove old statistics code
2019-12-25 12:02:18 +02:00
Hornwitser b2a902f784 Update Node.js requirement documentation
Update Node.js version specified in README.md to be explicit about which
versions below 12 is supported, and use version 12 by default.
2019-12-23 23:53:40 +01:00
Hornwitser f700e98e8e Remove old statistics code
Remove left over code from the old statistics gathering and display
which was replaced with Prometheus+Grafana.
2019-12-22 00:54:45 +01:00
Hornwitser 770c961900 Merge pull request #270 from Hornwitser/plugin-rewrite
Plugin system rewrite
2019-12-21 00:49:02 +01:00
Hornwitser 1873f9bda8 Remove UPSdisplay plugin
Remove the redundant UPSdisplay plugin.  UPS statistics is gathered by
the statistics_exporter plugin.
2019-12-20 21:26:29 +01:00
Hornwitser 26eefb0476 Rewrite statistics export
Add automatic export of metrics from slaves to the master server.  And
rewrite the statistics export plugin to use this new system.
2019-12-20 21:26:29 +01:00
Hornwitser 8229d1955c Skip broken Prometheus client tests
Skip Prometheus client tests that assume the environment is isolated
from the rest of the tests.
2019-12-20 19:14:35 +01:00
Hornwitser b4c7824101 Add homebrew Prometheus client
Add an implementation of a Prometheus client.  This is because the
prom-client implementation seems to have missunderstood how Prometheus
is supposed to work and doesn't allow updating collectors via
asynchronous callback.
2019-12-20 19:14:35 +01:00
Hornwitser 1f6574a89c Improve master shutdown
Close active connections and http servers during master shutdown and let
node exit on its own.  If this fails with an error set output streams to
blocking and report it then shutdown with process.exit.

Also add double SIGINT handling to immediately shutdown.
2019-12-20 02:37:27 +01:00
Hornwitser 8b9bb59357 Fix fast-test
Fix incorrect locations for slowTest in clusterio integration tests and
add slowTest to the patch integration tests.  Fixes fast-test being
broken.
2019-12-14 17:53:34 +01:00
Hornwitser b71c392b32 Port globalChat to new plugin system
Port the globalChat plugin to the new plugin system.  Since there's no
config system yet the config options for it is removed.
2019-12-14 17:51:54 +01:00
Hornwitser bcceae687f Reimplement plugin system
Implement a new plugin system based subclassing pre-defined base clases
and communicating with pre-defined link messages.
2019-12-14 17:48:56 +01:00
Hornwitser 513651393b Implement forwarding to instances for events
Implement forward to instance for Event messages.
2019-12-14 17:31:02 +01:00
Hornwitser e5115df28b Implement broadcast for events
Add broadcastTo property to events that broadcasts them to all entities
of the specified target that's downstream to the connection it was sent
to.
2019-12-13 22:37:58 +01:00
Hornwitser 21d9fdff7b Remove old plugin system
Remove the old client/instance fused plugin system along with
instanceManagement in client.js and getPlugins in master.js.
2019-12-13 22:37:49 +01:00
Hornwitser 22c325394f Merge pull request #269 from Hornwitser/link-refactor-2
Another link refactor
2019-12-13 22:29:57 +01:00
Hornwitser ca21ff741b Add changelog entry for communication rewrite
Add entry to changelog detailing the rewrite of the communication layer.
2019-12-13 21:57:05 +01:00
Hornwitser d4dd290654 Add communication layer docs
Add breif description of the link, connector and messages communication
concepts implemented by lib/link.
2019-12-13 21:57:05 +01:00
Hornwitser f0afbf0a33 Complete test coverage for lib/link/link.js
Add tests for the remaining uncovered lines in lib/link/link.js.
2019-12-13 21:57:05 +01:00
Hornwitser 5dd1490c75 Fix clusterctl error on exit
Fix code still using the close method on the Link class which was
removed in 3bccbac.
2019-12-13 21:57:05 +01:00
Hornwitser 9c8ae2bf06 Remove debug logging
Remove left over verbose debug logging.
2019-12-13 21:57:05 +01:00
Hornwitser 5d8fa4e4ce Refactor instance communication
Change Instance class to be a subclass of Link and refactor the
communication to and establisment of instances to use a virtual
connection between the slave and the instance.  This is simplifies the
implementation of plugin communication for instances.

Replace InstanceRequest with a forwardTo option to Request which does
the task of forwarding in a much cleaner way.
2019-12-13 21:57:05 +01:00
Hornwitser 9f24d200b7 Ignore config in top level directory only
Change .gitignore to only ignore the config files in the top level
directory of repository.  If these files exist elsewhere it's likely
either intended or an error.
2019-12-13 21:57:05 +01:00
Hornwitser 47cff048f0 Improve link error messages
Reverse the reversal of source/target in message errors and add link
source and target to the error given when a validator is missing on a
link.
2019-12-13 21:39:54 +01:00
Hornwitser b24778dce6 Add filter argument to getNewestFile
Add filter predicate to allow filtering out files that are not desired
from getNewestFile.  This is useful for filtering out .tmp.zip files.
2019-12-13 21:39:54 +01:00
Hornwitser 62afcaadab Use shared StartupError class
Remove separate StartupError class definition in master.js in favor of
the shared StartupError class in lib/error.js.
2019-12-13 21:39:54 +01:00
Hornwitser b59fd80a7b Merge pull request #268 from Hornwitser/port-1.2.4
Port 1.2.4 fixes
2019-12-13 21:37:17 +01:00
Hornwitser 7a03b84858 Port fix bcrypt failing to install on Windows
The maintainers of bcrypt have in their infinite wisdom decided to stop
distributing Windows binaries.  Their inner workings remain a mystery.

This is a port of 5fe98a0.
2019-12-11 02:14:00 +01:00
Survaaki a2d10c8c02 Add changelog entry from 1.2.4
Add changelog entry for the change in the removed master mod uploading.
2019-12-11 02:14:00 +01:00
Hornwitser 3e7f497669 Port remove obsolete flow statistics from clusterioMod
Remove the obsolete and broken statistics gather from the clusterioMod
plugin.  The statisticsExporter plugin already exports all of the
statistics (and more) that was exported here.

This is a port of 3e7d4a8.
2019-12-11 02:14:00 +01:00
Hornwitser f1ac41bc4c Port fix researchSync updating researched technologies
Add check in filter_updated_techs that ignores already researched
technologies.

This is a port of b0eb188.
2019-12-11 02:14:00 +01:00
Hornwitser b2239aeb96 Add changelog entry from 1.2.4
Add changelog entry for the removal of the combinator signaling which
was first implemented in the master branch.
2019-12-11 02:13:57 +01:00
Hornwitser 34d4604bab Merge pull request #265 from Hornwitser/link-refactor
lib/link refactor and plugin preparations
2019-11-30 00:53:42 +01:00
Hornwitser ea4f3ab2bf Remove uncoloured formatting of factorioOutput
Since chalk autodetects the color support in the terminal there is no
need for a non-coloured version of formatOutput.
2019-11-30 00:08:10 +01:00
Hornwitser c6fd46e3c3 Replace sources/targets with links in messages
Replace the sources/targets concept with a links array in the message
definitions.  Fixes the problem of the actual links the messages applies
to being ambigious when there's multiple sources and targets.
2019-11-30 00:08:10 +01:00
Hornwitser 3be264c9a9 Add integration testing of commands
Add testing of the clusterctl commands against the master and slave
code.
2019-11-30 00:08:10 +01:00
Hornwitser b24c129e38 Improve formating for instance output
Use colours instead of dim in the coloured output from clusterctl as not
all terminals support dim.
2019-11-30 00:08:10 +01:00
Hornwitser f5d2b058d9 Fix deleteInstance returning early
Fix deleetInstance returning success before the instance have actually
been deleted.
2019-11-30 00:08:10 +01:00
Hornwitser 3f86011db1 Disable instance plugins
Disable the completely broken instance plugins as they break the
integration tests.
2019-11-30 00:08:10 +01:00
Hornwitser f8aa947a98 Add tests for resolveInstance
Add tests for the resolveInstance function in clusterctl.
2019-11-30 00:08:10 +01:00
Hornwitser bfff297d9a Add tests for clusterctl's output formatting
Add tests for formatOutput and formatOutputColored by using the same
lines as the line parser uses for testing.
2019-11-30 00:08:10 +01:00
Hornwitser 3bccbac496 Refactor to a connector based concept in lib/link
Remove connection and client subclasses of Link and put the connection
logic into a "connector" class.  The connector deals with sending
and receiving messages, handling the connection and reconnection of lost
connections and giving sequence numbers to messages.

This move the logic of the actual connection away from the link
sub-classes, which in turn means the connection should be a lot easier
to mock in tests.
2019-11-30 00:08:10 +01:00
Hornwitser a3eb39712c Make FactorioServer.exampleSettings static
Convert the exampleSettings method to be a static method.
2019-11-30 00:08:10 +01:00
Hornwitser 039b41d3e6 Move events and requests into messages
Merge the events and requests categories in lib/link into messages and
use the class name of the message to determine which handler to call.

Also swaps the order of the arguments for forwardInstanceRequest to be
consistent with the rest of the lib/link code and fixes up the docs for
requests and events.
2019-11-30 00:08:10 +01:00
Hornwitser 278dc9b2d2 Remove empty instanceManager file
Remove the empty/incomplete instanceManager implementation.
2019-11-30 00:08:10 +01:00
Hornwitser 2d3307c045 Remove unused binary option from plugin configs
Remove the unimplemented "binary" option from all plugin config files.
Setting this option to something other than "nodePackage" would
supposedly run a binary and feed it with the stdout of the factorio
proccess, but that was never implemented.
2019-11-30 00:08:10 +01:00
Hornwitser 5228e0a4d7 Merge pull request #261 from Hornwitser/daemonize
Daemonize Slaves
2019-11-25 23:15:15 +01:00
Hornwitser d9910b0750 Merge pull request #263 from Hornwitser/contributing-docs
Add contributing instructions
2019-11-25 23:13:53 +01:00
Hornwitser 1dea5fddbe Forward Factorio logs to clusterctl
Add instance output event that is forwarded by the master server to
control connections that are subscribing to them.  Add code subscribing
to the relevant instance output when dealing with instances in
clusterctl.
2019-11-23 15:18:20 +01:00
Hornwitser f2c3006046 Remove obsolete fileOps code
Remove sync functions that should not be used as well as of functions
that provide functionality that is already implemented by fs-extra.
2019-11-23 15:18:20 +01:00
Hornwitser 554cab4fb0 Add SIGKILL detection to lib/factorio/server
Check if Factorio terminated as a result of SIGKILL and throw an error
suggesting the system may be low on memory as a cause.  Also print exit
code if the unexpected shutdown occured without a signal.
2019-11-23 15:18:20 +01:00
Hornwitser 7a2abaa5db Daemonize slave
Rework client.js to support running multiple instances at once and be
managed remotely and implement the neccessary communication
infrastructure for this.

Due to circular dependencies on the implementation of this the master
branch is significantly broken after this.
2019-11-23 15:16:47 +01:00
Hornwitser 5858d97857 Remove playerManager specific CLI tool
Remove the playerManager plugin specific CLI tool delete_players.js
2019-11-21 22:25:15 +01:00
Hornwitser bc41f7afa6 Clean up generateSSLcert test
Remove dependency to fileOps in generateSSLcert test file and clean up
the code to follow a higher standard.
2019-11-21 22:25:15 +01:00
Hornwitser 65f75d86e6 Add id based database load and save functions
Add functions to save and load a Map of objects indexed by the id
property in the objects.
2019-11-21 22:25:15 +01:00
Hornwitser fcae3ef0c1 Use detected Factorio version
Remove Factorio_version from the config file and use the detected
version of Factorio instead.
2019-11-21 22:25:11 +01:00
Hornwitser 7b2ba58701 Add contributing instructions
Add instructions detailing the recommended git workflow as well as some
code style guidelines.
2019-11-21 17:11:57 +01:00
Hornwitser 27fc68da0f Remove remoteCommands plugin and interface
Remove the half baked remote rcon command interface in preparation for a
better version based on a new socket.io communication layer.
2019-11-20 22:37:25 +01:00
Hornwitser ca2967e255 Fix clusterioMod item autosaving
Fix the autosave code being broken by the move in e4aae88.
2019-11-20 22:37:25 +01:00
Hornwitser d38e9a53c4 Remove broken combinator signaling
Remove badly implemented and broken remote combinator signaling.
2019-11-20 22:37:25 +01:00
Hornwitser d2d058e7d6 Remove the broken serverManager plugin
The remote management interface will completely change with the
implementation of #201.
2019-11-20 22:37:25 +01:00
Hornwitser 2f3b4c1aa8 Remove redundant API call in globalChat
Remove call to getInstanceName which in turn calls the rather expensive
/api/slaves in the globalChat plugin.
2019-11-20 22:37:25 +01:00
Hornwitser f3171c2feb Remove support for uploading mods to master
Remove the uploadMod and checkMod APIs as these have become more or less
obsolete after Factorio implementd syncing mods to the server in game.
2019-11-20 22:37:23 +01:00
Hornwitser 1d26212e1d Remove port and password arguments to client.js
Remove undocumented command line options set the ports and password used
in the instance.  These are not compatible with remote management.
2019-11-20 22:36:14 +01:00
Hornwitser 0474355175 Fix README port from 1.2.3
Fix the new instructions for installing clusterio on Windows not being
corrected to install the master branch when it was ported over.
2019-11-20 22:34:10 +01:00
Hornwitser ba7626ff2d Merge pull request #255 from Hornwitser/port-1.2.3-fixes
Port 1.2.3 fixes
2019-11-18 01:54:14 +01:00
Hornwitser 707f06be76 Port update Windows install instructions
Remove the reference to the out of repo Windows install script and list
updated commands to install Clusterio stable in the readme itself.

This is a port of e1771ec.
2019-11-15 15:44:22 +01:00
Hornwitser 35f3f4f02b Add changelog entry from 1.2.3
Add the fixed Hotpatch changelog entry from 1.2.3.
2019-11-15 15:41:11 +01:00
Hornwitser 31ab3a09f6 Port add fileReadDelay to inventoryImports config
Fix the inventoryImports client side code never receiving the script
output by specifying fileReadDelay and thereby getting the old
behaviour.  This broke with the introduction of the tail mode script
output mode which requires a newline to be written at the end of
the output.

This is a port of d810b56.
2019-11-15 15:39:43 +01:00
Hornwitser 7c79ea44b6 Port fix inventoryImports command
Fix the inventory imports command trying to count up the removed quick
bar slots as well as using player_main instead of character_main.

Add warning that the lua commands file containing the unminified vesion
of the command has not been kept up to date.  There are multiple changes
that has been done previously so these have already diverged.

This is a port of 10414eb.
2019-11-15 15:39:20 +01:00
Hornwitser 7359f9075e Port increase limit of Json payloads to master server
The default limit of 100kb to JSON payloads of bodyParser can be
exceeded when running modded games with researchSync.  Increase the
limit to 10mb as a bandaid to fix this issue.

This is a port of 25eb5e5.
2019-11-15 15:38:56 +01:00
Hornwitser f4b2300d9b Port disable uploading of mods to master by default
Set uploadModsToMaster to false in config.json.dist.  This feature is
not necessary as the integrated mod portal in Factorio provides a better
method to synchronize mods.

This is a port of 3c5b108.
2019-11-15 15:38:22 +01:00
Hornwitser 680d196584 Merge pull request #251 from Hornwitser/server-robustness
Improve handling of FactorioServer.stop()
2019-10-28 22:53:10 +01:00
Hornwitser bdf8cd1152 Kill Factorio process if it's dected to hang
Send SIGKILL instead of SIGTERM when it's dectected that Factorio hangs
on shutdown.  This should hopefully resolve the travis ci hanging
issuse.
2019-10-28 22:30:57 +01:00
Hornwitser 246a8031c1 Add hang detection to FactorioServer.stop()
Attempt to detect if the Factorio server has hanged in
FactorioServer.stop() and send a second SIGTERM if this is the case.
2019-10-28 21:58:54 +01:00
Hornwitser a781c2f988 Wait for RCON connecting on FactorioServer.stop()
If the RCON client is currently in the process of connecting to the
Factorio server then wait for it to complete the connection to avoid it
emmiting out an error connecting in FactorioServer.stop().
2019-10-28 21:58:05 +01:00
Hornwitser 9082b40abd Update rcon-client
Update rcon-client to 4.2.0 in order to remove workaround for issue with
rcon-client 4.1.0 that has been resolved.
2019-10-28 21:54:19 +01:00
Hornwitser f163e6a39b Merge pull request #250 from Hornwitser/code-cleanup
Code cleanup
2019-10-28 20:24:50 +01:00
Hornwitser 283d47d88b Debug ci failure
Output factorio integration log to the travis log.
2019-10-28 20:04:30 +01:00
Hornwitser be02e57012 Remove mkdirp dependency
Replace usage of mkdirp with fs-extra.mkdirp and remove the mkdirp
dependency and the unused mkdirp-promise dependency.
2019-10-28 19:46:22 +01:00
Hornwitser cb8d7e240f Remove more remanent remote map code
Remove JavaScript code exported on the web interface that relates to the
removed remote map plugin.
2019-10-28 19:46:22 +01:00
Hornwitser 6f5534c45b Test for numeric strings in addItem and removeItem
Add tests asserting that numeric strings passed to the addItem and
removeItem methods to ItemDatabes throws.
2019-10-28 19:46:22 +01:00
Hornwitser 467249579d Add missing changelog entry for f4ca5d9
Add note in the changelog describing the feature added by f4ca5d9.
2019-10-28 19:46:22 +01:00
Hornwitser 3fdc27238b Make _doleDivisionFactor local
Turn _doleDivisionFactor into a variable local to the module instead of
implicitly global.
2019-10-28 19:46:22 +01:00
Hornwitser 72b16de227 Remove logging from averaged-timeseries
The logging implemented in averaged-timeseries consists solely of
printing "Logging enabled!".  Remove this pointless logging.
2019-10-28 19:46:22 +01:00
Hornwitser 9a2b2c9f8d Speed up generateSSLcert test
Significantly speed up the generateSSLtest by reducing the bits in the
key from 2048 to 512.
2019-10-28 19:46:22 +01:00
Hornwitser e4aae88614 Move clusterioMod specific code into the plugin
Move item database, tests, endpoint definitions for /api/place,
/api/remove, /api/inventory, /api/inventoryAsObject, and the web
interface view for the storage page into the clusterioMod plugin.
2019-10-28 19:46:22 +01:00
Hornwitser bc444b79c2 Remove broken client download command
Remove the client download command which would download pre-releases.
The mod add command is a complete replacement for it.
2019-10-28 19:46:21 +01:00
Hornwitser ff3fc3b94b Remove .js suffix in require paths
Remove the .js suffix in require paths to make requires consistent
across the project.
2019-10-28 19:45:34 +01:00
Hornwitser 6bde8a2219 Remove .js suffix from node invocations
Remove the .js suffix from help texts of node invocations as well as
actual invocations as this is not necessary.  Removing it opens up the
possibility of restructuring the code into a directory with the same
name.
2019-10-28 19:45:32 +01:00
Hornwitser a9a382b993 Mark package as 2.0.0 development version
Change the version in package.json to 2.0.0-alpha
2019-10-28 19:44:56 +01:00
Hornwitser e287ca6ea9 Merge pull request #248 from Hornwitser/save-patch
Replace Hotpatch with a save patcher
2019-10-27 17:23:27 +01:00
Hornwitser 5d9d3981ff Remove getLua and getCommand from clusterTools
Remove the unused and badly behaved getCommand and getLua functions from
clusterTools.  The main issue with these is that encourage the use of
large commands that have negative impact on the performance of the
servers, as such large commands have to be transmitted to all players
that are connected.  These functions also have poor handling of lua
comments and will fail on strings containing for example "--".
2019-10-24 22:49:16 +02:00
Hornwitser 1595555154 Refactor statisticsExporter lua code
Rewrite lua part of statisticsExporter to to use game.table_to_json and
the save patching based module system.
2019-10-24 22:49:16 +02:00
Hornwitser 1a035e849e Replace Hotpatch with save patching
Remove Hotpatch and implement a save patcher in its place.  The save
patcher takes a normal Factorio save and adds modules to it that's
loaded by a generated control.lua file.  This greatly reduces the
difficulty of adding an existing game to Clusterio and using scenario
code in plugins.

The mechanism for selecting what code to patch into the saves from
plugins is temorary and expected to change with #220.
2019-10-24 22:49:16 +02:00
Hornwitser 079e31cf05 Implement hashStream
Add a hashStream method to lib/hash that does the same as hashFile only
with a stream instead of a file path and implement tests for both of
these methods.
2019-10-24 22:49:16 +02:00
Hornwitser e6d995d97a Merge pull request #242 from Hornwitser/finish-factorio-server
Rewrite Factorio server spawning and communication
2019-10-24 22:48:19 +02:00
Hornwitser c08d97e698 Document change to instance ports and password
Add entry to changelog detailing the change to null by default entries
for the ports and password in the instance config, and update the
comment in it to reflect the recent changes.
2019-10-24 15:14:40 +02:00
Hornwitser 4d94fb9a6c Fix blank port/password being sent to master
Fix client sending a blank port/password when these are configured as
null and generated by the FactorioServer class.
2019-10-24 15:14:34 +02:00
Hornwitser 93480c8b29 Refactor getFactorioLocale
Move getFactorioLocale to the new lib/factorio namespace and refactor it
to use async functions instead of callbacks.  Split tests into
integration with factorio and testing of the function itself.

Also removes the cacheing from the function itself as this breaks the
isolated testing of it.
2019-10-14 18:56:46 +02:00
Hornwitser a01b971ba2 Rewrite Factorio server spawning
Break out the server process management to a new FactorioServer class in
lib/factorio.  This class takes responsibility for managing a factorio
server, communicating with it and handling the stopping of it.  It also
replaces the previous effort to do this in lib/factorioServer.

This change also updates the version of rcon-client, and since the
author of it has updated it to use UTF-8 the fork is no longer
necessary.  It also removes the messageInterface implementation
in favor of just calling the rcon-client library directly since
this has rate limiting logic built in.

The lib/factorio library is intended to abstract all the interfacing
that happens with Factorio.
2019-10-14 18:56:46 +02:00
Hornwitser 6b4ab6660c Generate instance ID separate from rcon password
Create a random instance ID using Math.random instead of deriving it
from the RCON password with a hombrew hash algorithm.  Remove
stringUtils as it's no longer needed.
2019-10-14 15:51:45 +02:00
Hornwitser 33e9f9d983 Link lib/scenarios instead of copying it
Link the instance directory's scenario folder to lib/scenarios instead
of copying it.  This ensures it stays up to date.
2019-10-14 15:51:45 +02:00
Hornwitser cbcf868c9e Refactor fileOps.getNewestFile
Simplify getNewestFile by converting it to an async function, use the
fs-extra promise based fs functions and remove the files argument as
well as the callback.

Refactors the tests as well to use the test/temp folder.
2019-10-14 15:51:45 +02:00
Hornwitser 6a870be233 Implement symlinking of shared mods
Add symlinkMods function which takes an instance and a shared mods
folder as input and updates or creates the instance mods folder from it
by symlinking the mods in the shared mods folder to the instance mods
folder.

On Windows this uses hard links as the symlinks require elevated
privileges.
2019-10-14 15:51:45 +02:00
Hornwitser 2ff52a1cf2 Move clusterioMod file creation
Move the creation of the output.txt, orders.txt and txbuffer.txt script
output files out of the client.js code and into the clusterioMod.
2019-10-14 15:51:45 +02:00
Hornwitser 3202e2d150 Merge pull request #246 from Hornwitser/port-1.2.2-fixes
Port 1.2.2 fixes
2019-10-13 17:57:26 +02:00
Hornwitser debb33eb3f Change instructions to install master branch
Change back the instructions in README.md to install the master branch
of clusterio and add warnings for this in the relevant locations
directing users to the stable branch for instructions on how to install
the stable version.
2019-10-13 15:36:06 +02:00
Hornwitser 34fc6f0820 Port exit if masterAuthToken is missing
Exit client.js with a non-zero exit code if it's detected that the
masterAuthToken is missing in the config.

This is a port of 8b4ec58.
2019-10-13 14:45:27 +02:00
Hornwitser e8d9d160c4 Port recommend running without pm2 first
Recommend running master server without pm2 first as this makes
discovering any potential errors much simpler.

This is a port of 6e26d32.
2019-10-13 14:45:27 +02:00
Hornwitser 999fb79570 Fix changelog entries for 1.2.2
Move node v10 requirement to 1.2.2 and add changelog entry for updating
bcrypt (which has already been done for the master branch.)
2019-10-13 14:19:35 +02:00
Hornwitser 77571d3c71 Port swap curl with wget in install instructions
Swap out curl with wget in the install instructions as wget is
pre-installed on Ubuntu.

This is a port of 72b56a0 and dbef2bd.
2019-10-13 14:19:15 +02:00
Hornwitser de20635aea Port reorder install instructions
Reorder the install pm2 step to be after the npm install step in the
instructions.  This is necessary to avoid npm creating some files in the
home directory owned as root and then failing the npm install step
because of this.

This is a port of c25bf23.
2019-10-12 22:53:44 +02:00
Hornwitser 5d24329456 Port fix crash in recount_cluster_research_progress
Check that the relevant research is present in the research mapping
before accessing it in recount_cluster_research_progress.

This is a port of 0cdb5d0.
2019-10-12 22:46:08 +02:00
Hornwitser 4eda983cf4 Port filter out lower levels when syncing research
Fix the next level of an infinite research getting updated with the
progress of the previous level from other nodes in the cluster by
filtering out infinite researches with a lower level than the local
level.

This is a port of 075d54f.
2019-10-12 22:45:43 +02:00
Hornwitser 7f82680511 Port set progress when completing infinite researches
Set the progress for the next level of an infinite research when
unlocking it via a script.  This is necessary when the research is
actively being researched  as Factorio will carry over the progress for
the current level into the next one when set via a script.

It's possible for the new progress to be non-zero if the next level was
on the research queue.

This is a port of 62f3a99.
2019-10-12 22:45:07 +02:00
Hornwitser 9098b3cf19 Port rename research to tech
Use tech as the loop variable for researches as this is consistent with
the rest of the researchSync module.

This is a port of 3b1ed1a
2019-10-12 22:44:13 +02:00
Hornwitser 183902ced3 Port fix incorrect property checks in researchSync
Fix naive checks for properties being set of the form obj[key] into the
proper form Object.hasOwnProperty.call(obj, key).  The reason for this
is that if the key has a value of a built-in property like 'constructor'
then a check of the form obj[key] will always return true even if that
property hasn't been set on the object.

This is a port of 6a6274e.
2019-10-12 22:43:27 +02:00
Arvids Godjuks b3eae22221 Merge pull request #241 from Hornwitser/port-1.2.1-fixes
Port version 1.2.1 fixes
2019-09-19 15:43:08 +02:00
Hornwitser 0b3e4fe6f4 Port fix license in package.json
Correct license entry being ISC instead of MIT in package.json.

This is a port of 7e2ca58.
2019-09-19 14:50:21 +02:00
Hornwitser 7d1942b615 Port spawn factorio server deteached in client.js
Sending ^C via the terminal on Linux may cause SIGINT to be sent twice
to the factorio process, causing it to exit without completing the
saving.  This happens because the shell sends the interupt to all
members of the process group for the job and this includes both the
node client process and the factorio server process.

Avoid this situation by passing detached to spawn when the platform is
Linux, which causes the process to be created with its own process group
and therefore not targeted by a SIGINT from the shell.

This unfortunately does not work on Windows as we can't send SIGINT or
^C to a process there.  Doing subprocces.kill on Windows terminates the
process similarly to how SIGKILL works on Linux.

This is a port of 6b090cd.
2019-09-19 14:50:21 +02:00
Hornwitser 9eb9291314 Port update node-factorio-api to 0.3.8
Note that the versions 0.3.6 and 0.3.7 had issues with an old
mod_cache.json being present.

This is a port of 78de723.
2019-09-19 14:50:21 +02:00
Arvids Godjuks b068550775 Merge pull request #238 from Hornwitser/move-templates
Move EJS templates
2019-09-18 18:16:00 +02:00
Hornwitser 6726c2b16f Refactor EJS string gluing code
Rewrite awful string gluing of HTML with JS to use the slightly less
awful EJS string gluing method.
2019-09-18 16:54:26 +02:00
Hornwitser 706a1dbd39 Indent static/browserconfig.xml
Make static/browserconfig.xml readable by indenting it's content.
2019-09-18 16:54:26 +02:00
Hornwitser a62e8d17a3 Move ejs templates to views/
Move the ejs templates to the default Express.js views/ folder and
change their extension to .ejs.  Reconfigure Express.js to look for
templates in the views folders as well as the plugins folder.  This
enables plugins to render templates using paths of following format
"pluginName/path/to/page-or-view".
2019-09-18 16:54:11 +02:00
Arvids Godjuks a4983d75fb Merge pull request #237 from Hornwitser/missing-coverage
Add missing files to coverage report
2019-09-18 15:35:29 +02:00
Hornwitser 1fd8ae954b Add missing files to coverage report
Add missing.js test files that requires any files that currently does
not have any unit tests defined for them.  This should hopefully make it
so that when unit tests are added for parts of these files the coverage
will increase instead of decreasing.

Note that this does not include CLI Tools/ as those are not testable.
2019-09-18 15:25:19 +02:00
Arvids Godjuks e15462439a Merge pull request #235 from Hornwitser/config-part-3
Config Rewrite Part 3
2019-09-18 14:06:50 +02:00
Arvids Godjuks 312c45bda5 Merge pull request #236 from Hornwitser/dedup-clustertools
Remove duplicated clusterTools code
2019-09-18 14:05:05 +02:00
Hornwitser aaf43143df Add compression to getInstanceName
Set the compression header when doing requests in getInstanceName.
2019-09-18 13:55:01 +02:00
Hornwitser c5cae30d83 Remove duplicated clusterTools code
Remove getCommand, getInstanceName, getSafeLua, and
checkHotpatchInstallation definitions from plugins and use the
implementation in lib/clusterTools for these functions instead.
2019-09-18 13:54:22 +02:00
Hornwitser a783bd9651 Refactor out config injection in modManager
Change config in modManager to be passed to each individual functions
instead of requiring it be present at load time.
2019-09-16 16:04:08 +02:00
Hornwitser 5f33cf4d3b Remove config management
Remove lib/manager/configManager.js and the config management command
line interface as well as the serverManager's web interface for config
management.  The configuration rewrite will work differently and is not
compatible with configManager.js.
2019-09-16 16:04:08 +02:00
Hornwitser dd865b42b5 Fix incorrect handling of instance ids
After 651b09b the instance ID would be treated differently depending on
wheteher it was a string or a number in the master server.  This caused
researchSync to break as it would send the ID as a number while the
master server would have it stored as a string.

Fix by coercing instanceID to a string in the master HTTP API.
2019-09-16 16:04:08 +02:00
Hornwitser 858449c57a Move client config loading to startClient
Move the configuration loading step in client.js into the startClient
function in order to separate it from the client code definition.
2019-09-16 16:04:08 +02:00
Hornwitser 53a5ce062b Ensure instance name is a valid Windows file name
Add check for instance name that ensures it being a valid Windows file.
See the microsoft article about naming files for more details:
https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file
2019-09-16 16:04:02 +02:00
Hornwitser 1ba9e68603 Move instance directory to Instance class
Replace the instancedirectory variable with an Instance class that holds
the directory and has a .path() to get paths relative to the instance
directory.
2019-09-15 13:43:14 +02:00
Arvids Godjuks ec223e9d9e Merge pull request #234 from Hornwitser/move-tests
Move unit tests to test/ folder
2019-09-14 18:21:09 +02:00
Hornwitser a63c341209 Move unit tests to test/ folder
Move unit tests for client, master, and the lib to test/ and move plugin
unit tests to <plugin dir>/test.
2019-09-14 14:49:52 +02:00
Arvids Godjuks 74c99b9e13 Merge pull request #233 from Hornwitser/client-refactor-and-fixes
Client refactor and fixes
2019-09-14 02:14:38 +02:00
Hornwitser bcb22d2af7 Securely generate rcon password
Replace Math.random() based password generation for the rcon interface
with a more secure method based crypto.randomBytes.
2019-09-14 01:18:51 +02:00
Hornwitser eb1b863cec Remove unimplemented mods update command
Remove the mods update command from the client command line interface as
this calls the non-existant modManager.updateAllMods function.
2019-09-14 01:15:36 +02:00
Hornwitser 2428cba2bd Default to ports in the dynamic range
Use random ports in the dynamic range as define by RFC 6335 for
instaces.
2019-09-14 01:15:01 +02:00
Hornwitser 500c9584eb Remove commented out itemStats code
Remove old commented out code dealing with item statistics that is no
longer relevant.
2019-09-14 00:56:22 +02:00
Hornwitser 18a904d9de Use as-table for listInstances
Rewrite listInstances to use as-table instead of manually coded table
generation.
2019-09-14 00:54:32 +02:00
Hornwitser 85daf6df19 Rename listPlugins to listInstances
Rename incorrectly named function listPlugins to listInstances.
2019-09-14 00:53:50 +02:00
Arvids Godjuks a57b0a3b5e Merge pull request #231 from Hornwitser/auth-master
Port socket.io Authentication to master
2019-09-11 17:52:10 +02:00
Hornwitser 8aa66e9e68 Port authentication of socket.io server
Authenticate connections made to the socket.io server via a token query
parameter that's validated in a similar manner to the x-access-token for
HTTP requests.

This is a port of 0c45e57.
2019-09-11 16:59:11 +02:00
Hornwitser d378287941 Restructure Changelog
Make "Changelog" the top level header, and move the existing headers one
level down.
2019-09-10 23:53:21 +02:00
AlienXAXS ea5c0d5aa3 Fixes editSlaveMeta and db AddItem errors (#228)
Fixed editSlaveMeta to pass a number to db.addItem method
2019-09-10 17:52:09 +02:00
Arvids Godjuks 094d44a7f5 Merge pull request #227 from Hornwitser/config-part-2
Config Rewrite Part 2
2019-09-09 14:06:50 +02:00
Hornwitser 1bbf1542d2 Refactor client.js command handling into functions
Move the various actions performed by client.js into separate async
functions and do the command action logic if client.js is invoked as a
module directly.
2019-09-09 12:44:09 +02:00
Hornwitser 98900e72dc Merge masterIP and masterPort into masterURL
Merge the masterIP and masterPort configuration entries into a single
masterURL entry.  Fixes the problem with connecting to an HTTPS version
of the master server and also makes it easier to host the master server
behind a reverse proxy.

This breaks all known code interfacing Clusterio.
2019-09-09 12:44:09 +02:00
Hornwitser da99b39402 Remove compiled TypeScript code
Remove the compiled output of veladon's clusterioModel project.  The
source code can be found at https://github.com/veladon/clusterioModel
and doesn't really belong in Clusterio as it's specific to the train
teleports plugin.
2019-09-06 15:43:44 +02:00
Arvids Godjuks aba22960a6 Merge pull request #226 from Hornwitser/check-tests
Check the exit status of the tests script
2019-09-06 12:55:17 +02:00
Hornwitser 0ec37414c2 Deduplicate test script and check exit status
Remove the explicit ignoring of the exit status of the test scripts.
This makes the Travis CI build be considered a failure if one of the
tests fail.  Also rewrite the fast-test script to use the test script.
2019-09-06 00:16:13 +02:00
Arvids Godjuks 2e50921d32 Documentation adjustments (#225)
README.md update - adjusted install instruction to be against the stable branch
2019-09-05 14:55:42 +02:00
TheAIBot e14f7c22fd Merge pull request #222 from Hornwitser/config-part-1
Config Rewrite Part 1
2019-09-05 11:33:43 +02:00
Hornwitser a31778c7ff Remove unused config parameter to pluginManager
Remove the unused library initialization parameter config to
pluginManager and reorganize pluginManager to no longer be defined by a
function.
2019-09-04 20:43:13 +02:00
Hornwitser 35546b4b18 Move loading of config in master to startServer
Move the loading of the config file in the master server to the
startServer function.  Remove config.json.test as it's no longer needed.
2019-09-04 20:43:13 +02:00
Hornwitser 651b09b613 Refactor database and item retreival
Change the ugly global slaves object to be a member of the db object and
implement the items database as a separate integrety verifying class in
lib/database.
2019-09-04 20:43:13 +02:00
Arvids Godjuks e1263a40b1 Merge pull request #221 from Hornwitser/fix-socketio
Fix master socket.io broken by a3fc1dd
2019-09-04 18:28:22 +02:00
Hornwitser 23ce86aef6 Fix master socket.io broken by a3fc1dd
Add missing attach calls to the master socket.io object that was
accidentally removed by the refactoring in a3fc1dd.  Unfortunately I've
been unable to determine if socket.io actually supports listening on
multiple servers at the same time, this needs further investigation.
2019-09-04 17:39:49 +02:00
Hornwitser 5d6f6de379 Export app as a separate entry from master.js
Change the module exports from master.js to allow additional objects to
be passed to allow better unit testing.
2019-09-04 17:36:06 +02:00
Hornwitser af2471b427 Remove config dependency in lib/authenticate
Restructure lib/authenticate to set the authentication secret at a later
point in time via setAuthSecret.  This breaks playerManager.
2019-09-04 17:36:06 +02:00
Arvids Godjuks 8c1c8d07a9 Merge pull request #224 from Hornwitser/duck-tape-researchsync
Duck tape researchsync tests
2019-09-04 17:34:28 +02:00
Arvids Godjuks b0ad02d412 Merge pull request #219 from Hornwitser/dev-notice
Add development warning
2019-09-04 17:32:51 +02:00
Arvids Godjuks 7d61cd9716 Updated README.md to warn about master usage
And checkout the 1.2.x branch instead or use the release.
2019-09-04 16:22:58 +02:00
Hornwitser 31298c7c1c Duct tape together researchSync tests
Add nock that responds to the requests the researchSync module makes
during the tests, and disable broken tests.
2019-09-04 14:59:25 +02:00
Hornwitser b191f7d831 Use instanceName in researchSync
Remove silly roundabout way to the get the current instance name.
2019-09-04 14:59:21 +02:00
Hornwitser f4ca5d92a1 Pass instance name to plugins
Add instanceName entry to the config passed to plugins that contains the
name of the current instance.
2019-09-04 14:59:01 +02:00
Hornwitser 703423ae54 Add development branch warning at startup
Add a big banner based warning telling this is the development branch
for clusterio to the startup of client.js and master.js.
2019-09-01 23:34:52 +02:00
Hornwitser 84f48e36f4 Improve error reporting at startup
Add StartupError that differentiates unhandled errors from errors with
known causes and use it to only print the "please report" banner when an
unhandled error occurs.

Also makes the error reporting more visible in the console.
2019-09-01 23:24:32 +02:00
Hornwitser 6ee84c41bf Add development branch notice
Add notice to the top of README.md stating this is the development
branch for 2.0 and that the 1.2.x branch should be used for production
systems.
2019-08-30 19:34:23 +02:00
Arvids Godjuks ef66ab37da Merge pull request #218 from Hornwitser/remove-remanent-remotemap
Remove remanent remoteMap code
2019-08-29 14:36:51 +02:00
Hornwitser b73b202513 Remove remanent remoteMap code
Remove remoteMap specific code left behind after the removal of the
remoteMap plugin in 6415784.  The plugin interface should be versatile
enough to allow all of this code to reside inside the plugin itself.
2019-08-29 11:38:57 +02:00
Arvids Godjuks 5d28718175 Merge pull request #215 from Hornwitser/various-fixes
Various unrelated fixes and improvements
2019-08-24 16:36:26 +02:00
Hornwitser 3b08b73c37 Remove unused function parseJsString
Remove unused and dangerous parseJsString function from
inventoryImports.
2019-08-24 11:55:19 +02:00
Hornwitser 9c9b4463a4 Fix nested headers in static/nodes
Fix instance names being nested in both h2 and h5 headers, which is not
allowed in html.
2019-08-24 11:35:57 +02:00
Hornwitser fb2519cb50 Add fast-test and fast-cover npm scripts
Add alternate test and coverage scripts that skips tests that are slow.
2019-08-24 11:25:40 +02:00
Hornwitser 5a9e2a8825 Fix inventoryImports/interfacingFunctions test
Fix test in inventoryImports/interfacingFunctions breaking due to
masterAuthSecret not being defined in the config.
2019-08-24 11:25:40 +02:00
Hornwitser a39a17b220 Throw Error objects in inventoryImports/interfacingFunctions
Change throwing of strings objcts to throw proper Error objects.
2019-08-24 11:25:40 +02:00
Hornwitser 940a7b9180 Refactor getFactorioLocale spec
Fix the incorrect error validation, be more lenient about empty values
and convert the checks to use async functions instead of error prone
callbacks.

Remove excessive getFactorioLocale checks in master.spec.
2019-08-24 11:25:40 +02:00
Hornwitser ab2a377ca5 Remove Prometheus pushgateway code
Remove code pushing metrics to a hardcoded Prometheus pushgateway that
is no longer in operation.  There's no point supporting the use of a
pushgateway as there's already a full client implemented and since the
master server is not ephemeral or batch job it shouldn't be using a
pushgateway in the first place.
2019-08-24 11:25:40 +02:00
Hornwitser ea2f291025 Remove reference to old authentication scheme
Remove long comment and commented out code about an old sanity check in
/api/place.
2019-08-24 11:25:40 +02:00
Hornwitser 3e80e40223 Move database and masterMods folder creation
Move the code creating the database and masterMods folders into the
startServer function in master.js.
2019-08-24 11:25:39 +02:00
Hornwitser ed54db0db9 Handle errors during master startup
Print a message urging unhandled exceptions thrown by startup to be
reported to the GitHub issue tracker and initiate shutdown.
2019-08-24 11:09:01 +02:00
Hornwitser 7dcd46a99a Add .vs to .gitignore
Prevent Visual Studio workspace files from getting accidentally added to
the project.
2019-08-24 11:04:58 +02:00
Hornwitser 0d4ad2f075 Fix global leaks detected by mocha
Fix globals leaked in stringUtils, isJson and static/nav.html.
2019-08-24 11:02:50 +02:00
Hornwitser 0a9ddef120 Check for globals leaking during test
Add --check-leaks to mocha to have it report globals that leak during
the testing.
2019-08-24 11:02:50 +02:00
Hornwitser 38462d3069 Set User-Agent according to GitHub's request
GitHub requests that the user agent be set to either the name of the
user or the application[1].  Set user agent to the name of this
application as they request.

[1] https://developer.github.com/v3/#user-agent-required
2019-08-24 11:02:50 +02:00
Hornwitser 0398bd10f8 Remove debug code
Remove debug switch and console.log guarded by it to log every post
request to /api/getId as it's not needed.
2019-08-24 11:02:50 +02:00
Arvids Godjuks 55164cc48f Merge pull request #213 from Hornwitser/ssl-gen-refactor
Refactor SSL certificate creation
2019-08-23 17:44:07 +02:00
Hornwitser 44b7d0aa19 Refactor SSL certificate creation
Rewrite the generateSSLcert lib to be async, use the sslCert and
sslPrivKey config entries and invoke it only if needed during master
server startup instead of in the post install script.  Also add note to
the changelog covering both this and the fix in c645fac.
2019-08-23 17:24:39 +02:00
Arvids Godjuks cc87654742 Merge pull request #212 from Hornwitser/revert-nonsense
Revert "Use old doledivision as default, new NN based dole is optional"
2019-08-23 17:04:20 +02:00
Hornwitser ec7a93c14f Revert "Use old doledivision as default, new NN based dole is optional"
Revert nonsensical commit 2ef6c93b05
adding personal Visual Studio workspace files.
2019-08-23 16:51:14 +02:00
Arvids Godjuks ca03bed8b7 Merge pull request #211 from clusterio/Remove-hardcoded-paths
Replace hardcoded paths with paths from the config
2019-08-23 15:41:58 +02:00
Arvids Godjuks 88739a952a Merge pull request #210 from Hornwitser/update-node
Update Node.js and dependencies
2019-08-23 15:39:41 +02:00
TheAIBot c645fac3de Replace hardcoded paths with paths from the config 2019-08-23 15:08:20 +02:00
Hornwitser c1699fe33d Update dependencies
Update old dependencies.  Fix change in mocha causing it to not exist
afer the tests are done and a change in the command parsing causing the
tests to no longer be detected.
2019-08-23 14:51:10 +02:00
Hornwitser 69a1ff7817 Use Node.js version 10
Update instructions, Dockerfile and Travis CI to use Node.js 10.
Specify that Node.js version 10 or greater is needed in the engines
section of package.json (note that this is only advisory.)  And document
this as a breaking change in CHANGELOG.md.
2019-08-23 14:50:22 +02:00
Arvids Godjuks e4386d57e8 Merge pull request #209 from Hornwitser/cleanup-travis
Cleanup travis and coverage script
2019-08-23 14:42:07 +02:00
Hornwitser bfa8a99daa Cleanup travis and coverage script
Do not install git, tar and curl as these packages are installed by
default on Travis CI.  Move the codecov upload to the travis script and
do not include the codecov token as it's not needed when uploaded from
Travis CI.  Make the cover npm script into something that can be runned
locally and move the Travis CI version to a new ci-cover script.  Remove
the verbose flag from the tar extration of the factorio headless server.
2019-08-23 14:17:56 +02:00
Arvids Godjuks d72bcbdf01 Merge pull request #204 from Hornwitser/refactor-master
Refactor master to fix unit tests
2019-08-23 14:45:47 +03:00
Hornwitser 71c07fa527 Change GitHub references (#205)
Change references to Danielv123/factorioClusterio on GitHub to
clusterio/factorioClusterio.
2019-08-21 23:18:17 +02:00
Hornwitser 8898e923de Fix travis test integration
Add config.json.test as the the config for running the tests on travis
and add script command to copy it to config.json there.  Should fix the
unit test for master failing at loading the config.
2019-08-21 12:43:09 +02:00
Hornwitser a3fc1dda8c Refactor master.js
Move server initialization code to startServer and call it only when
master.js is being runned directly.  Also refactor the server listen
step to work properly when https is configured and only listen after
plugins have completed loading.

Remove the async function wrapper around the master.js code.  This is
just dumb and makes the code untestable.
2019-08-21 12:43:09 +02:00
Hornwitser a828f50e57 Refactor master.spec.js
Ensure exceptions are properly passed down to the unit test framework
with promises, check the return status of requests, use config to
generate an access token, reduce code duplication and reformat to be
consistent.
2019-08-21 12:43:09 +02:00
Hornwitser 442472e412 Actually fix masterPlugins visibility
The masterPlugins array should not be a global.  The ejs templates
already have it provided through locals.masterPlugins and plugins get it
through onLoadFinish.  Make masterPlugins local to master.js again and
fix the ejs templates to use locals.masterPlugins.
2019-08-21 12:43:09 +02:00
4028 changed files with 24318 additions and 14120 deletions

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
packages/master/static
plugins/*/static
+276
View File
@@ -0,0 +1,276 @@
"use strict";
module.exports = {
"env": {
"node": true,
"commonjs": true,
"es6": true,
},
"parserOptions": {
"ecmaVersion": 11,
},
"overrides": [
{
"files": ["test/**/*.js", "plugins/*/test/**/*.js"],
"env": {
"mocha": "true",
},
"rules": {
"prefer-arrow-callback": "off",
"no-invalid-this": "off",
},
},
{
"files": ["packages/master/web/**/*.jsx"],
"env": {
"browser": true,
},
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"jsx": true,
},
},
},
],
"rules": {
"accessor-pairs": "error",
"array-bracket-newline": "off",
"array-bracket-spacing": ["error", "never"],
"array-callback-return": "error",
"array-element-newline": "off",
"arrow-body-style": "error",
"arrow-parens": "off",
"arrow-spacing": ["error", { "after": true, "before": true }],
"block-scoped-var": "error",
"block-spacing": "error",
"brace-style": ["warn", "1tbs", { "allowSingleLine": true }],
"callback-return": "off",
"camelcase": "off",
"capitalized-comments": "off",
"class-methods-use-this": "off",
"comma-dangle": [
"error",
{
"arrays": "always-multiline",
"objects": "always-multiline",
"imports": "always-multiline",
"exports": "always-multiline",
"functions": "only-multiline",
},
],
"comma-spacing": "warn",
"comma-style": ["error", "last"],
"complexity": "warn",
"computed-property-spacing": ["error", "never"],
"consistent-return": "error",
"consistent-this": "error",
"curly": "warn",
"default-case": "error",
"default-case-last": "error",
"default-param-last": "error",
"dot-location": ["error", "property"],
"dot-notation": ["warn", { "allowKeywords": true }],
"eol-last": "error",
"eqeqeq": "error",
"func-call-spacing": "error",
"func-name-matching": "error",
"func-names": "off",
"func-style": ["error", "declaration"],
"function-call-argument-newline": "off",
"function-paren-newline": "off",
"generator-star-spacing": ["error", { "before": false, "after": true }],
"global-require": "warn",
"grouped-accessor-pairs": "error",
"guard-for-in": "error",
"handle-callback-err": "error",
"id-blacklist": "error",
"id-length": "off",
"id-match": "error",
"implicit-arrow-linebreak": "error",
"indent": ["error", "tab", { "SwitchCase": 1 }],
"indent-legacy": "off",
"init-declarations": "off",
"jsx-quotes": "error",
"key-spacing": "error",
"keyword-spacing": "warn",
"line-comment-position": "off",
"linebreak-style": ["error", "unix"],
"lines-around-comment": "off",
"lines-around-directive": "off",
"lines-between-class-members": ["error", "always"],
"max-classes-per-file": "off",
"max-depth": "warn",
"max-len": ["warn", { "code": 120 }],
"max-lines": "off",
"max-lines-per-function": "off",
"max-nested-callbacks": "error",
"max-params": "off",
"max-statements": "off",
"max-statements-per-line": "off",
"multiline-comment-style": ["warn", "separate-lines"],
"multiline-ternary": "off",
"new-cap": "error",
"new-parens": "error",
"newline-after-var": "off",
"newline-before-return": "off",
"newline-per-chained-call": "off",
"no-alert": "error",
"no-array-constructor": "error",
"no-await-in-loop": "off",
"no-bitwise": "off",
"no-buffer-constructor": "error",
"no-caller": "error",
"no-catch-shadow": "error",
"no-confusing-arrow": "error",
"no-console": "off",
"no-constant-condition": ["error", { "checkLoops": false }],
"no-constructor-return": "error",
"no-continue": "off",
"no-div-regex": "error",
"no-duplicate-imports": "error",
"no-else-return": "warn",
"no-empty-function": "off",
"no-eq-null": "error",
"no-eval": "error",
"no-extend-native": "error",
"no-extra-bind": "error",
"no-extra-label": "error",
"no-extra-parens": "off",
"no-floating-decimal": "error",
"no-implicit-coercion": "error",
"no-implicit-globals": "error",
"no-implied-eval": "error",
"no-inline-comments": "off",
"no-invalid-this": "warn",
"no-iterator": "error",
"no-label-var": "error",
"no-lone-blocks": "error",
"no-lonely-if": "error",
"no-loop-func": "error",
"no-magic-numbers": "off",
"no-mixed-operators": [
"error",
{
"groups": [
["&", "|", "^", "~", "<<", ">>", ">>>"],
["==", "!=", "===", "!==", ">", ">=", "<", "<="],
["in", "instanceof"],
],
},
],
"no-mixed-requires": "error",
"no-multi-assign": "error",
"no-multi-spaces": "error",
"no-multi-str": "error",
"no-multiple-empty-lines": "error",
"no-native-reassign": "error",
"no-negated-condition": "off",
"no-negated-in-lhs": "error",
"no-nested-ternary": "error",
"no-new": "error",
"no-new-func": "error",
"no-new-object": "error",
"no-new-require": "error",
"no-new-wrappers": "error",
"no-octal-escape": "error",
"no-param-reassign": "off",
"no-path-concat": "error",
"no-plusplus": ["warn", { "allowForLoopAfterthoughts": true }],
"no-process-env": "error",
"no-process-exit": "warn",
"no-proto": "error",
"no-restricted-exports": "error",
"no-restricted-globals": "error",
"no-restricted-imports": "error",
"no-restricted-modules": "error",
"no-restricted-properties": "error",
"no-restricted-syntax": "error",
"no-return-assign": "error",
"no-return-await": "off",
"no-script-url": "error",
"no-self-compare": "error",
"no-sequences": "error",
"no-shadow": ["error", { "allow": ["yargs"] }],
"no-spaced-func": "error",
"no-sync": "error",
"no-tabs": ["error", { "allowIndentationTabs": true }],
"no-template-curly-in-string": "error",
"no-ternary": "off",
"no-throw-literal": "error",
"no-trailing-spaces": "error",
"no-undef-init": "error",
"no-undefined": "off",
"no-unmodified-loop-condition": "error",
"no-unneeded-ternary": "error",
"no-unused-expressions": "error",
"no-use-before-define": "warn",
"no-useless-backreference": "error",
"no-useless-call": "error",
"no-useless-computed-key": "error",
"no-useless-concat": "error",
"no-useless-constructor": "error",
"no-useless-rename": "error",
"no-useless-return": "error",
"no-var": "warn",
"no-void": "error",
"no-warning-comments": "off",
"no-whitespace-before-property": "error",
"nonblock-statement-body-position": "error",
"object-curly-newline": "error",
"object-curly-spacing": "off",
"object-shorthand": "off",
"one-var": "off",
"one-var-declaration-per-line": "error",
"operator-assignment": "off",
"operator-linebreak": "warn",
"padded-blocks": "off",
"padding-line-between-statements": "error",
"prefer-arrow-callback": "warn",
"prefer-const": "off",
"prefer-destructuring": "off",
"prefer-exponentiation-operator": "error",
"prefer-named-capture-group": "off",
"prefer-numeric-literals": "error",
"prefer-object-spread": "error",
"prefer-promise-reject-errors": "error",
"prefer-reflect": "off",
"prefer-regex-literals": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"prefer-template": "warn",
"quote-props": "off",
"quotes": ["error", "double", { "avoidEscape": true }],
"radix": ["error", "always"],
"require-atomic-updates": "off", // XXX Not sure why this triggers on process.title = ...
"require-await": "off",
"require-jsdoc": "off",
"require-unicode-regexp": "off",
"rest-spread-spacing": ["error", "never"],
"semi": "error",
"semi-spacing": "error",
"semi-style": "off",
"sort-imports": "off",
"sort-keys": "off",
"sort-vars": "error",
"space-before-blocks": "warn",
"space-before-function-paren": "off",
"space-in-parens": ["error", "never"],
"space-infix-ops": "off",
"space-unary-ops": "error",
"spaced-comment": ["warn", "always"],
"strict": "error",
"switch-colon-spacing": "error",
"symbol-description": "error",
"template-curly-spacing": ["error", "never"],
"template-tag-spacing": "error",
"unicode-bom": ["error", "never"],
"valid-jsdoc": ["error", { "requireReturn": false }],
"vars-on-top": "warn",
"wrap-iife": "error",
"wrap-regex": "off",
"yield-star-spacing": "error",
"yoda": ["error", "never", { "exceptRange": true }],
},
};
+14 -27
View File
@@ -25,43 +25,30 @@ coverage
build/Release
dist/
node_modules/*
# Autogenerated docs output
/docs/api
node_modules
Factorio*
/factorio
/instances
/sharedMods
/database
database/items.db
config.json
/plugin-list.json
/config-master.json
/config-control.json
/config-slave.json
package-lock.json
secret-api-token.txt
mod_cache.json
static/pictures/spritesheet.js
static/pictures/spritesheet.png
static/external
static/export
saves
/temp/
# 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
# Web build
/packages/master/static
/plugins/*/static
# IDE
.idea/
.vs/
+15
View File
@@ -0,0 +1,15 @@
language: node_js
node_js:
- 10
before_install:
- curl -o factorio.tar.gz -L https://www.factorio.com/get-download/latest/headless/linux64
- tar -xf factorio.tar.gz
- npm install -g codecov
script:
- npx lerna bootstrap --no-ci
- node packages/lib/build_mod --source-dir packages/slave/lua/clusterio_lib --bump-patch --factorio-version 0.18 --output-dir temp/test/sharedMods
- curl -o temp/test/sharedMods/subspace_storage_1.99.1.zip -L https://github.com/clusterio/factorioClusterioMod/releases/download/1.99.1/subspace_storage_1.99.1.zip
- npm run-script ci-cover
- cat temp/test/integration/log.txt
- codecov -f coverage/lcov.info
- npm run-script lint
-3
View File
@@ -1,3 +0,0 @@
{
"CurrentProjectSetting": null
}
-8
View File
@@ -1,8 +0,0 @@
{
"ExpandedNodes": [
"",
"\\sharedPlugins"
],
"SelectedNode": "\\.gitignore",
"PreviewInSolutionExplorer": false
}
File diff suppressed because it is too large. Load diff
BIN
View File
Binary file not shown.
+173
View File
@@ -1,6 +1,179 @@
Changelog
=========
Version 2.0.0
-------------
### Major Features
- Replaced Hotpatch with save patching. Removed the Hotpatch scenario and
the depency on it for getting code into the game. Added a save patcher than
runs before starting up Factorio that patches in lua modules based on the
event_loader lib into the savegame. Regular freeplay games can now be
used with Clusterio and will be compatible without having to convert them
to Hotpatch.
- Daemonized slaves. Slaves now have the ability to run multiple Factorio
instances, starting and stopping them individually. To manage this the
local command line interface has been replaced with a remote interface on
the master server that can be accessed through the clusterioctl cli tool.
- Rewritten the communication between slaves and the master from scratch. The
new system is based on a WebSocket connection between the slaves and the
master server and provides efficient validated bi-directonal communication.
- Rewritten the plugin system from scratch. Plugins now inherit from
a base class and use the same WebSocket connection Clusterio uses to
communicate.
- New configuration system with support for initializing configs when needed,
modifying config entries using built-in commands, and updating instance
configuration remotely.
- Packaged and uploaded to npm again, greatly simplifying distribution and
installation of both Clusterio and plugins.
### Features
- Added export of pollution statitics.
- Reconnection logic that esures no data is dropped talking to the master
server provided the session can be resumed.
- Added support for having multiple Factorio installs and selecting which
version to use on a per instance basis.
- Added config to specify which path the master interface is accessed under,
allowing it to be proxied behind web-server.
- Added WebSocket usage statistics.
- Added commands sent statistic.
- Added Factorio server CPU and memory usage statistic.
- Added Factorio autosave size statistic.
- Added support for extracting locale and item icons from Factorio and mods.
- Added integrated user management for controlling access and storing per
player data.
- Added synchronization of in-game adminlist, banlist and whitelist to
Clusterio core. Previously this was handled by the playerManager plugin.
### Changes
- Fixed sslCert and sslPrivKey entries being ignored in the config.
- Changed ssl key creation to be done at startup instead of on npm install.
- Added error handling during master startup.
- Factorio game and rcon port now defaults to a random port above 49151.
- Removed unimplemented mods update command.
- Fixed rcon password being generated with Math.random().
- Added plugins directory to the views path. This makes it possible for
plugins to render their own ejs views or pages in their own folders by
using paths of the format "pluginName/path/to/page-or-view".
- Replaced the per instance copy of the shared Factorio mods with
symlinks. On Windows hard links are used instead due to the
privileges requirements of symlinks.
- Changed the per instance scenario folder to be linked instead of
copied on instance creation.
- Instance id is no longer derived from the rcon password, instead it's
generated upon instance creation and stored in the instance config.
- The game port, RCON port and RCON password instance config entries are are
now null by default, indicating that a random one will be generated every
time the instance is started.
- Removed the broken client download command as this would download
pre-releases.
- Moved the item database and HTTP definitions for /api/place, /api/remove,
/api/inventory, /api/inventoryAsObject and the web interface view for the
storage page into the clusterioMod plugin. If you disable this plugin
then these things will not be available.
- Removed undocumented --port and --rcon-port arguments to client.
- Removed undocumented FACTORIOPORT and RCONPORT enviroment variable
handling in client.
- Removed redundant call to /api/slaves in globalChat plugin.
- Removed broken serverManager plugin.
- Removed factorio_version from config. The version installed is auto
detected and used instead.
- Removed the playerManager specific command CLI tools/delete_player.js.
- Creating an instance, assigning it to a slave, creating a save for an
instance and starting an instance is now four separate commands.
- Removed oddball limits to slaves.json size.
- Moved slave specific and instance specific configuration into their own
configuration files.
- Removed unused binary option from plugin config.
- Removed info and shout command from globalChat plugin
- Removed mirrorAllChat and enableCrossServerShout configuration options for
globalChat plugin.
- Removed UPSdisplay plugin. UPS statistics is exported by the statistics
exporter plugin.
- Master server now defaults to hosting on https on port 8443.
- Renamed client to slave.
### Breaking Changes
- Removed lib/authenticate. Breaks playerManager.
- The masterIP and masterPort config entries has been merged into masterURL.
Breaks discordChat, playerManager, serverSelect, trainTeleports and
clusterioModel.
- Instance names can no longer be invalid Windows file names.
- Removed config management from the command line and the server manager.
- Moved ejs templates into views folder and changed their extension to
.ejs. Breaks playerManager.
- Mods are no longer copied from the per instance instanceMods directory.
If you need per instance mods you can now place them directly in the mods
directory inside the instance folder.
- Hotpatch scenarios and code loading is no longer compatible with Clusterio.
Breaks playerManager, serverSelect, and tranTeleports.
- Removed getLua and getCommand from lib/clusterTools. If you need to run
more than the most trivial of code in commands use the save patcher and
add in a remote interface.
- Removed mod uploading and distributing from the HTTP interface. Breaks
the old unmaintained and no longer needed factorioClusterioClient.
- Removed the remoteCommands plugin and the old runCommand interface. Breaks
playerManager and external tools sending commands.
- Removed broken serverManager plugin.
- Removed fields info, time, rconPort, rconPassword, serverPort, unique,
mods, instanceName, playerCount, mac, and meta from the slaves in the
slave database.
- Removed getInstanceName and lib/clusterTools. Breaks playerManager,
and discordChat.
- Removed the /api/rconPasswords, and /api/slaves endpoints. Breaks web
interface, trainTeleports, and discordChat.
- Removed the /api/getSlaveMeta and /api/editSlaveMeta endpoints. Breaks
researchSync, and UPSdisplay.
- Removed the hello event from the socket connection handshake. Breaks
playerManager, trainTeleports, serverSelect, and discordChat
- Changed the format of database/slaves.json.
- Removed the output file subscription system. Breaks inventoryImports,
playerManager, trainTeleports, serverSelect and researchSync.
- Removed the factorioOutput hook from instance plugins. The onOutput
hook provides parsed output instead.
- Removed the onLoadFinish hook from master plugins.
- Moved plugins from the sharedPlugins directory to plugins directory.
- Implemented a new plugin system that replaces the old. Breaks all plugins.
- Removed express metric http_request_duration_milliseconds metric.
- Removed socket.io metrics socket_io_connected, socket_io_connect_total,
socket_io_disconnect_total, socket_io_events_received_total,
socket_io_events_sent_total, socket_io_recieve_bytes, and
socket_io_transmit_bytes,
- Removed clusterio_connected_instaces_gauge and added
clusterio_master_connected_clients_count in its place.
- Renamed clusterio_player_count_gauge to
clusterio_statistics_exporter_instance_player_count
- Removed clusterio_UPS_gauge and added
clusterio_statistics_exporter_instance_game_ticks_total in its place.
- Renamed clusterio_endpoint_hit_gauge to clusterio_http_enpoint_hits_total
- Renamed clusterio_statistics_gauge to
clusterio_statistics_exporter_instance_force_flows
- Renamed clusterio_nn_dole_gauge to clusterio_subspace_storage_nn_dole_gauge.
- Renamed clusterio_dole_factor_gauge to
clusterio_subspace_storage_dole_factor_gauge.
- Renamed clusterio_import_gauge to clusterio_subspace_storage_import_total.
- Renamed clusterio_export_gauge to clusterio_subspace_storage_export_total.
- Renamed clusterio_master_inventory_gauge to
clusterio_subspace_storage_master_inventory.
- Removed clusterioMod plugin specific config options logItemTransfers,
disableFairItemDistribution useNeuralNetDoleDivider, autosaveInterval, and
disableImportsOfEverythingExceptElectricity from the master config.
- Removed msBetweenCommands config option. The RCON is instead limited to 5
concurrent commands.
- Removed allowRemoteCommandExecution config option. Remote commands are
always allowed with the move to master managing slaves/instances.
- Removed `--databaseDirectory`, `--masterPort`, and `--sslPort` command line
arguments from the master server.
- Implemented a new config system that replaces the old. Breaks all plugins.
- Removed usage of socket.io entirely in favor of a plain WebSocket connection.
- Renamed clusterioMod plugin to subspace_storage.
- Master no longer creates secret-api-token.txt.
Version 1.2.4
-------------
-41
View File
@@ -1,41 +0,0 @@
const needle = require("needle");
const cli = require("cli");
const config = require("./../config.json");
let options = cli.parse()
cli.setUsage(`node cluster_rcon.js /c game.player.force = "player"`)
if(process.argv.length < 3){
cli.getUsage()
process.exit(1)
}
const masterIP = `${config.masterIP}:${config.masterPort}`;
const token = config.masterAuthToken
let commandArray = process.argv;
commandArray.shift();
commandArray.shift();
const needleOptionsWithTokenAuthHeader = {
headers: {
'x-access-token': token,
},
};
let command = commandArray.join(" ");
let killBiters = `/silent-command local surface=game.surfaces[1] for key, entity in pairs(surface.find_entities_filtered({force="enemy"})) do entity.destroy() end`;
(async ()=>{
needle.post(masterIP+"/api/runCommand", {
broadcast:true,
command,
}, needleOptionsWithTokenAuthHeader, (err, resp) => {
console.log(resp.body)
});
// setInterval(()=>{
// needle.post(masterIP+"/api/runCommand", {
// broadcast:true,
// command: killBiters,
// }, needleOptionsWithTokenAuthHeader, (err, resp) => {
// console.log(resp.body)
// });
// }, 1000*60*5);
})();
-19
View File
@@ -1,19 +0,0 @@
const needle = require("needle")
const config = require("./../config.json")
const cli = require("cli")
let options = cli.parse({
name: ["n", "Name of the player to delete, case sensitive", "string"],
token: [false, "Factorio auth token", "string", config.masterAuthToken]
})
if(!options.name){
cli.getUsage()
process.exit(1)
}
needle.post(config.masterIP+":"+config.masterPort+"/api/playerManager/deletePlayer", {
name: options.name,
token: options.token
}, (err, resp) => {
console.log(resp.body)
});
+6 -4
View File
@@ -1,15 +1,17 @@
FROM node:10
# Warning: This is currently broken as a result of
# changes done in the 2.0 refactoring.
FROM node:12
RUN apt-get update && apt install git curl tar -y
RUN mkdir factorioClusterio
RUN git clone -b master https://github.com/Danielv123/factorioClusterio.git && cd factorioClusterio && npm install --only=production
RUN git clone -b master https://github.com/clusterio/factorioClusterio.git && cd factorioClusterio && npm install --only=production
RUN cd factorioClusterio && curl -o factorio.tar.gz -L https://www.factorio.com/get-download/latest/headless/linux64 && tar -xf factorio.tar.gz
WORKDIR factorioClusterio
RUN mkdir instances sharedMods
RUN cp config.json.dist config.json
RUN node client.js download
RUN node client manage shared mods add clusterio
LABEL maintainer "Sir3lit@gmail.com"
@@ -18,4 +20,4 @@ VOLUME /factorioClusterio/instances
VOLUME /factorioClusterio/sharedMods
VOLUME /factorioClusterio/sharedPlugins
CMD RCONPORT="$RCONPORT" FACTORIOPORT="$FACTORIOPORT" MODE="$MODE" node $MODE\.js start $INSTANCE
CMD MODE="$MODE" node $MODE start $INSTANCE
-5
View File
@@ -19,8 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
The icons for angels modded items located in /static/pictures/ are licensed
by https://mods.factorio.com/user/Arch666Angel under CC BY-NC-ND 4.0
https://creativecommons.org/licenses/by-nc-nd/4.0/
+245 -179
View File
@@ -8,248 +8,347 @@
Discord for development/support/play: https://discord.gg/5XuDkje
# ABANDONED VERSION
## Important notice
**The 1.2.x version of Clusterio has been abandonded.
Please go to the [main branch of the project](https://github.com/clusterio/clusterio) and continue from there.
The information below is out of date and kept for archival purposes only.**
This is the development branch for factorioClusterio 2.0 which is currently undergoing heavy
restructuring and refactoring. Expect plugins and existing installations to frequently break when
using this branch. If you don't want to be an alpha tester for 2.0 please use the stable
[1.2.x branch][1.2.x] or
[latest stable release](https://github.com/clusterio/factorioClusterio/releases/latest).
Installation instructions below are for the unstable master branch. Go to the page for
the [1.2.x branch][1.2.x] for instructions on how to install the stable version.
[1.2.x]: https://github.com/clusterio/factorioClusterio/tree/1.2.x
### Ways to support me/the project:
* Contribute with code/documentation. Always nice to move the project forward
* Contribute with code/documentation. See [Contributing](docs/contributing.md) for how to make pull
requests. Always nice to move the project forward.
* Support me monetarily on [patreon](https://www.patreon.com/danielv123) or paypal: danielv@live.no
### Table of contents
* [Introduction & methodology](#introduction)
* [Introduction](#introduction)
* [Features](#features)
* [Plugins](#plugins)
* [Ubuntu setup](#ubuntu-setup)
* [Windows setup](#windows-setup)
* [Optional plugins](#Plugins)
* [Installing Plugins](#installing-plugins)
* [Configure Master Server](#configure-master-server)
* [Hosting Behind Proxy](#hosting-behind-proxy)
* [Setting up an admin account](#setting-up-an-admin-account)
* [Running Clusterio](#running-clusterio)
* [Master Server](#master-server)
* [Slaves](#slaves)
* [Instances](#instances)
* [Common problems](#Common-problems)
* [Command cheatsheet](#cheatsheet)
## Introduction
Features:
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/factorioClusterioMod).
- 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
By itself Clusterio does 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.
Connection diagram:
## Features
![http://i.imgur.com/7FdVfgB.png](http://i.imgur.com/7FdVfgB.png)
- 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.
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.
- User list management for synchronizing in-game admins, whitelisted
users, and bans to all the servers in the cluster.
**How does it work?**
- Integrated support for exporting statistics for the whole cluster to
Prometheus via a single metrics endpoint.
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:
- Extensive plugin support for adding your own cross server features to
Factorio using Clusterio's communication backbone.
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
## Plugins
3. master: server1 gave us stuff, store it in the storage and write some statistics
The heart of Clusterio is its plugin syste. Plugins add functionality
to Factorio servers, Clusterio itself or both. These are the plugins
supported and maintained by the Clusterio developers:
4. server2: get-chest is empty, write a request to file
- [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 master server.
- [Subspace Storage](https://github.com/clusterio/factorioClusterioMod):
Provide shared storage that can transport items between servers via
teleport chests.
5. client.js on server2: Request file has stuff in it, parse and send a request to master for more items of that type
Want to make your own plugin? Check out the documentation on [Writing
Plugins](/docs/writing-plugins.md) for where to start.
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, independent on what mods are used. Yes, modded items are fully supported.
Clusterio can also do a few other neat things, such as giving you access to epoch time, syncing player inventories between servers, keeping track of playtime (playerManager plugin), teleporting trains between servers (trainTeleports) and exporting tons of factorio related statistics to Prometheus for graphing in grafana.
## Ubuntu setup
NodeJS does not support EOL ubuntu releases. Make sure you are on the most recent LTS release or newer.
**Warning**: These instructions are for the unstable master version and is not
recommended for use, see [the 1.2.x branch][1.2.x] for how to install the stable version.
Clusterio runs on Node.js v12 and up, and v10.16.0+. Node.js itself is not
supported on EOL Ubuntu releases so make sure you're on a recent release of Ubuntu.
Master and all slaves:
wget -qO - https://deb.nodesource.com/setup_10.x | sudo -E bash -
sudo apt install -y nodejs python-dev git build-essential
git clone -b 1.2.x https://github.com/Danielv123/factorioClusterio.git
cd factorioClusterio
wget -qO - https://deb.nodesource.com/setup_12.x | sudo -E bash -
sudo apt install -y nodejs
mkdir clusterio
cd clusterio
npm init -y
npm install @clusterio/master @clusterio/slave @clusterio/ctl
wget -O factorio.tar.gz https://www.factorio.com/get-download/latest/headless/linux64
tar -xf factorio.tar.gz
npm install --only=production
cp config.json.dist config.json
node ./lib/npmPostinstall.js
downloads and installs nodejs, git and clusterio. To specify a version, change "latest" in the link to a version number like 0.14.21.
Optional step (if you want to use pm2):
sudo npm install pm2 -g
Now you need to edit the `config.json` file. If you skip this step nothing will work.
Pretty much all the blank fields should be filled in, except on the master where a few can be omitted.
* You get the `masterAuthToken` from `secret-api-token.txt` in the master install dir after running the master twice.
* You get your factorio matchmaking token from factorio.com
* The `masterAuthSecret` should never be touched unless you want to invalidate everyones authentication tokens
**Master**
node master.js
Or with pm2 (it's recommened to run it without pm2 first):
pm2 start master.js --name master
**Server Host**
To download the mod for all its non vanilla features and items, (optional, but very recommended)
node client.js manage shared mods add clusterio
To create a new instance (its own save, set of mods and config files)
node client.js start [instancename]
To launch an instance with pm2
pm2 start --name [instancename] client.js -- start [instancename]
use `nano config.json` to change settings.
**Ubuntu with Docker**
The Docker support for Clusterio is curently broken. If you need it
open an issue about it.
<!--
Clusterio has *very* limited support for using docker.
sudo docker build -t clusterio --no-cache --force-rm factorioClusterio
sudo docker run --name master -e MODE=master -p 1234:8080 -d -it --restart=unless-stopped danielvestol/clusterio
sudo docker run --name slave -e MODE=client -e INSTANCE=world1 -v /srv/clusterio/instances:/factorioClusterio/instances -p 1235:34167 -it --restart=unless-stopped danielvestol/clusterio
sudo docker run --name slave -e MODE=slave -e INSTANCE=world1 -v /srv/clusterio/instances:/factorioClusterio/instances -p 1235:34167 -it --restart=unless-stopped danielvestol/clusterio
The -v flag is used to specify the instance directory. Your instances (save files etc) will be stored there.
-->
## Windows setup
Clusterio is built up of multiple parts. Here is a quick guide:
Master = master.js
Server host (Slave) = client.js + factorio server
Game Client = The people connecting to the server
**Warning**: These instructions are for the unstable master version and is not
recommended for use, see [the 1.2.x branch][1.2.x] for how to install the stable version.
**Requirements**
download and install nodeJS 10 from http://nodejs.org
download and install git from https://git-scm.com/
reboot when you are done, then proceed to the next steps. *reboots matter*
download and install nodeJS 12 from http://nodejs.org. Clusterio runs on Node.js v12 and
up, and v10.16.0+.
**Master**
1. Open PowerShell or Command prompt in the directory you want to install to and run the following commands.
git clone -b 1.2.x https://github.com/clusterio/factorioClusterio
cd factorioClusterio
npm install --only=production
copy config.json.dist config.json
mkdir clusterio
cd clusterio
npm init -y
npm install @clusterio/master @clusterio/slave @clusterio/ctl
2. Obtain Factorio by either of these two methods:
- Via the stand alone version on from their website
1. Download the MS Windows (64-bit zip package) from https://www.factorio.com/download
1. Create a new folder named "factorio" in the the factorioClusterio
folder.
2. Open the zip file and drag the folder called "Factorio_0.17.x" into the factorioClusterio folder
2. Download the MS Windows (64-bit zip package) from
https://www.factorio.com/download .
3. Rename the folder to "factorio"
3. Open the zip file and drag the folder called "Factorio_x.y.z" into
the factorio folder created in step 1.
- Via steam installation
1. Locate the game files by right clicking the game in steam, selecting properties, then Local Files, then Browse local files.
1. Create a new folder named "factorio" in the the factorioClusterio
folder.
2. Go to the parent folder of the folder that Steam opened and copy the Factorio folder into the factorioClusterio folder
2. Locate the game files by right clicking the game in steam,
selecting properties, then Local Files, then Browse local files.
3. Rename the folder to "factorio"
3. Go to the parent folder of the folder that Steam opened and copy
the Factorio folder into the factorio folder created in step 1.
3. Open `config.json` with a text editor and configure as desired.
## Installing Plugins
4. Run `node master.js` to generate the athentication token into secret-api-token.txt
Installing plugins to make them work with Clusterio consists of two
steps. First install the package via npm, for example
5. Run `node master.js` again to start the master server.
npm install @clusterio/plugin-subspace_storage
**Server Host**
Then tell clusterio that this plugin exists by adding it as a plugin.
1. Do step 1 and 2 of the Master section above *OR* use the same folder that was created in that section.
npx clusteriomaster plugin add @clusterio/plugin-subspace_storage
2. Open `config.json` with a text editor and configure as desired. You will need to set masterAuthToken to string found in secret-api-token.txt on the master server.
This adds it to the default `plugin-list.json` file which in the shared
folder setup is loaded by master, slave and ctl. If you have slaves or
ctl installed on separate computers (or directories) then you need to
repeat the plugin install process for all of them. The clusteriomaster,
clusterioslave and clusterioctl commands has the plugin sub-command
so you do not need to install clusteriomaster to add plugins.
3. Optionally run the command `node client.js manage shared mods add clusterio` to add the clusterio mod (needed for item teleports.)
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. Run `node client.js start [instancename]` to create a new instance. Repeat it again to start the instance.
To connect to a master server running on a remote machine, open config.json with your favourite text editor (notepad++). You can also set it up to use the official server browser.
## Configure Master Server
Change `masterIP `to something like `31.152.123.14` (provided by master server owner)
By default the master server will create a self signed TLS certificate
and listen on HTTPS on port 8443. You can change the port used with the
command
Change `masterPort` to something like `8080` (provided by master server owner)
npx clusteriomaster config set master.https_port 1234
Change `masterAuthToken` to the value found in `secret-api-token.txt` on the master server
If you plan to make your cluster available externally set the address
that it will be accessible under with, for example
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 clusteriomaster config set master.external_address https://192.0.2.4:8443/
**GameClient**
Change the url to reflect the IP, protocol, and port the master server
is accessible under, dns names are also supported. If proxyed under a
sub-path include the sub path as well and make sure the URL ends with a
slash.
Fancy game client that does the following steps automatically, but is really old so be warned: [clusterioClient](https://github.com/Danielv123/factorioClusterioClient)
You can list the config of the master server with the `npx
clusteriomaster config list` command. See the [readme for
@clusterio/master](/packages/master/README.md) for more
information.
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
2. Drop it into ./factorio/mods
### Hosting Behind Proxy
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
If you want to host the master server behind a reverse proxy the
recommended setup is to disable HTTPS and host it on a high port
over HTTP to which you point your proxy to. For example to configure
port 8080 as the HTTP port for the master server.
npx clusteriomaster config set master.https_port # unsets https_port
npx clusteriomaster config set master.http_port 8080
The proxy can then forward requests to a sub-path like `/cluster/` to
`http://localhost:8080/`.
Additianlly WebSocket connections to api/socket relative to the sub-path
the interface is hosted under also needs to be forwarded. See below for
the specifics of setting this up on Apache.
#### 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 /cluster/>
ProxyPass "http://localhost:8080/"
</location>
<Location /cluster/api/socket>
ProxyPass "ws://localhost:8080/api/socket"
</Location>
```
Pay attention to the use of trailing slashes, they are important.
### Setting up an admin account
Before you can manage the cluster you need to bootstrap an admin account
for it. Replace `<username>` with your Factorio username (do not make
up a new username here).
npx clusteriomaster bootstrap create-admin <username>
npx clusteriomaster bootstrap create-ctl-config <username>
The first command creates a user account with the given name and
promotes it to a cluster admin. The second one sets up a
`config-control.json` config for clusterioctl to connect to the master
server under the given user account.
## Running Clusterio
After following the installation and master configuration instructions
you can use the following commands to run Clusterio.
### Master Server
It's necessary to run the master server in order for anything to work.
Once you've completed the setup run the following command to start it
up:
npx clusteriomaster run
### Slaves
Slaves connect to the master server and are managed remotely from the
master server. In order for slaves to connect to the master server they
need a valid authentication token, you can create a slave config with
a valid token with the following command.
npx clusterioctl slave create-config --name Local --generate-token
This will write a new `config-slave.json` file in the current directory
(you can change the location with the `--output` option) with the name,
token and url to connect to the master server with. If you are making
the config for a remote slave you will need to have set the
`master.external_address` option to the URL the master server can be
reached on.
You can list the config of a slave on the slave itself with the `npx
clusterioslave config list` command. See the [readme for
@clusterio/slave](/packages/slave/README.md) for more information.
Once the config is set up run the slave with
npx clusterioslave run
### Instances
Instances are created, managed and started from the master server. For
now the only interface available is the `clusterioctl` command line tool
included in Clusterio. You can run this tool from any slave, or the
master server without having to set up a config, if you want to manage
the cluster from somewhere else you will need to set the
`control.master_url` and `control.master_token` options with the `node
clusterioctl control-config set` command:
The basic operations to start a new instance is the following
npx clusterioctl instance create "My instance"
npx clusterioctl instance assign "My instance" "Local"
npx clusterioctl instance start "My instance"
The first line creates the instance configuration on the master server.
The second assigns the instance to a slave which creates the instance
directory and files needed to run the instance on the given slave. The
third line starts the instance, which creates a new save if there are no
save games present.
There are many more commands available with clusterioctl. See the
[Managing a Cluster](/docs/managing-a-cluster.md) document or
`npx clusterioctl --help` for a full list of them.
## Plugins
Here are the known Clusterio plugins in the wild:
1. [Player Manager](https://github.com/Danielv123/playerManager) - Adds player management to the Web UI and shared inventory handling (beta)
2. [DiscordChat](https://github.com/jakedraddy/ClusterioDiscordChat) - Logs in-game chat/joins/leave messages on all instances to a Discord webhook.
3. [TrainTeleports](https://github.com/Godmave/clusterioTrainTeleports) - Allows you to teleport cargotrains between servers.
## Common problems
### Cannot find module: `/../../config`
Copy your config.json.dist to config.json and configure it.
### EACCESS [...] LISTEN 443
Some systems don't let non root processes listen to ports below 1000. Either run with `sudo` or change config.json to use higher port numbers.
@@ -273,36 +372,3 @@ TLDR: the tested fix is:
- select TCP and select specific local ports and type in the ports that you want to open (comma separated) and click next > 3 times
- give the rule a name (like 'web server' or something), give it a description (optionally) and click finish
### Other fixes for other potential problems:
Sometimes the install fails. Try `node ./lib/npmPostinstall.js` to complete it.
## Cheatsheet
**To create a new instance/start it**
node client.js start [instanceName]
**Other instance management tools:**
```
node client.js delete [instanceName]
node client.js list
```
**To update clusterio to the latest version:**
1. Download the latest zip version of factorio for your platform manually from factorio.com. Place it in the project root folder and call it "factorio" (folder name is specified in config.json)
2. Grab the latest version of the repo
```
git pull
npm install --only=production
```
3. Download the latest version of the factorioClusterioMod from its github repo
```
node client.js manage shared mods add clusterio
```
Binary file not shown.
-802
View File
@@ -1,802 +0,0 @@
const fs = require('fs-extra');
const Tail = require('tail').Tail;
const https = require('follow-redirects').https;
const needle = require("needle");
const child_process = require('child_process');
const path = require('path');
const syncRequest = require('sync-request');
const request = require("request");
const ncp = require('ncp').ncp;
const Rcon = require('rcon-client-fork').Rcon;
const deepmerge = require("deepmerge");
const getMac = require('getmac').getMac;
const rmdirSync = require('rmdir-sync');
const ioClient = require("socket.io-client");
// internal libraries
const objectOps = require("lib/objectOps.js");
const fileOps = require("lib/fileOps");
const stringUtils = require("lib/stringUtils.js");
const configManager = require("lib/manager/configManager.js");
const hashFile = require('lib/hash').hashFile;
// argument parsing
const args = require('minimist')(process.argv.slice(2));
// require config.json
var config = require(args.config || './config');
var global = {};
if (!fs.existsSync(config.instanceDirectory)) {
fs.mkdirSync(config.instanceDirectory);
}
if (!fs.existsSync("./sharedPlugins/")) {
fs.mkdirSync("sharedPlugins");
}
if (!fs.existsSync("./sharedMods/")) {
fs.mkdirSync("sharedMods");
}
const instance = process.argv[3];
const instancedirectory = config.instanceDirectory + '/' + instance;
const command = process.argv[2];
// Set the process title, shows up as the title of the CMD window on windows
// and as the process name in ps/top on linux.
process.title = "clusterioClient "+instance;
// add better stack traces on promise rejection
process.on('unhandledRejection', r => console.log(r));
// make sure we have the master access token (can't write to master without it since clusterio 2.0)
if(!config.masterAuthToken || typeof config.masterAuthToken !== "string"){
console.error("ERROR invalid config!");
console.error("Master server now needs an access token for write operations. As clusterio slaves depends \
upon this, please add your token to config.json in the field named masterAuthToken. \
You can retrieve your auth token from the master in secret-api-token.txt after running it once.");
process.exitCode = 1;
return;
}
const needleOptionsWithTokenAuthHeader = {
compressed:true,
headers: {
'x-access-token': config.masterAuthToken
},
};
var instanceInfo = {};
var commandBuffer=[];
// messageInterface Management
setInterval(function(){
let command=commandBuffer.shift();
if(command){
messageInterfaceInternal(command[0], command[1], command[2], command[3]);
}
},config.msBetweenCommands || 50);
// function to handle sending commands into the game
async function messageInterfaceInternal(command, callback, resolve, reject) {
// try to save us if you send a buffer instead of string
if(typeof command == "object") {
command = command.toString('utf8');
}
if(false && process.platform == "linux" && typeof command == "string" && serverprocess) {
/*
to send to stdin, use:
serverprocess.stdin.write("/c command;\n")
*/
serverprocess.stdin.write(command+"\n");
if(typeof callback == "function"){
callback();
}
resolve();
} else if(typeof command == "string" && client && client.send && typeof client.send == "function") {
try {
let str = await client.send(command+"\n");
if(typeof callback == "function") callback(str)
resolve(str)
} catch (err) {
console.log("RCON failed, server might be paused or shutting down");
// console.log(err);
// reject(err);
if(typeof callback == "function"){
callback();
}
reject(err)
}
}
}
function messageInterface(command, callback) {
return new Promise((resolve,reject) => {
commandBuffer.push([command,callback, resolve, reject]);
});
}
// handle commandline parameters
if (!command || command == "help" || command == "--help") {
console.error("Usage: ");
console.error("node client.js start [instance name]");
console.error("node client.js list");
console.error("node client.js delete [instance]");
console.error("To download the latest version of the Clusterio lua mod, do");
console.error("node client.js manage shared mods download clusterio");
console.error("To download a clusterio plugin, do");
console.error("node client.js manage shared plugins install https://github.com/Danielv123/playerManager");
console.error("For more management options, do");
console.error("node client.js manage");
process.exit(1);
} else if (command == "list") {
let instanceNames = fileOps.getDirectoriesSync(config.instanceDirectory);
instanceNames.unshift("Name:");
let longestInstanceName = 0;
// determine longest instance name
instanceNames.forEach(function(instance){
if(instance.length > longestInstanceName) longestInstanceName = instance.length;
});
let displayLines = [];
// push name coloumn to array
instanceNames.forEach(function(instance){
while(instance.length < longestInstanceName+1){
instance += " ";
}
displayLines.push("| "+ instance + "|");
});
// create port colloumn
let factorioPorts = [];
instanceNames.forEach(function(instance){
let factorioPort;
if(instance.includes("Name:")){
factorioPort = "Port:"
} else {
factorioPort = require(path.resolve(config.instanceDirectory, instance, 'config')).factorioPort;
}
factorioPorts.push(factorioPort);
});
factorioPorts.forEach((port, index) => {
let longestPort = 0;
factorioPorts.forEach((port, index) => {
if(port.toString().length > longestPort) longestPort = port.toString().length;
});
while(port.toString().length < longestPort){
port += " ";
}
factorioPorts[index] = port;
});
instanceNames.forEach(function(instance, index){
displayLines[index] += " " + factorioPorts[index] + " |";
});
displayLines.forEach(line => console.log(line));
process.exit(0);
} else if (command == "manage"){
// console.log("Launching mod manager");
//const fullUsage = 'node client.js manage [instance, "shared"] ["mods", "config"] ...';
function usage(instance, tool, action){
if(tool && tool == "mods"){
console.log('node client.js manage '+instance+' '+tool+' ["list", "search", "add", "remove", "update"]');
} else if(tool && tool == "config") {
console.log('node client.js manage '+instance+' '+tool+' ["list", "edit"]');
} else if(tool && tool == "plugins") {
console.log(`node client.js manage ${instance} ${tool} ["list", "add", "remove"]`);
} else {
console.log('node client.js manage '+(instance || '[instance, "shared"]') +' '+ (tool || '["mods", "config", "plugins"]') + ' ...');
}
}
const tool = process.argv[4] || "";
const action = process.argv[5] || "";
if(instance){
if(tool == "mods"){
(async function(){try{
// do require down here to reduce master load time
const modManager = require("lib/manager/modManager.js")(config);
// allow managing mods
if(action == "list"){
console.log(await modManager.listMods(instance));
} else if(action == "search"){
console.log(await modManager.findMods(process.argv[6]));
} else if(action == "add" || action == "download"){
await modManager.addMod(process.argv[6], instance);
} else if(action == "remove" || action == "rm" || action == "delete"){
await modManager.removeMod(process.argv[6], instance);
} else if(action == "update"){
await modManager.updateAllMods();
} else {
usage(instance, tool);
}
process.exit(0);
}catch(e){
console.log("Got error from modManager:")
console.log(e);
}})();
} else if(tool == "config"){
// allow managing the config
if(action == "list" || action == "show" || action == "display"){
configManager.displayConfig(instance);
} else if(action == "edit"){
let newConfigValue = "";
process.argv.forEach((arg, i)=>{
if(i >= 8){
newConfigValue += " "+arg;
} else if(i >= 7){
newConfigValue += arg;
}
});
configManager.editConfig(instance, process.argv[6], newConfigValue);
} else {
usage(instance, tool);
}
} else if(tool == "plugins"){
(async function(){try{
const pluginManager = require("lib/manager/pluginManager.js")(config);
if(action == "list"){
await pluginManager.listPlugins();
} else if(action == "add" || action == "install" || action == "download"){
let status = await pluginManager.addPlugin(process.argv[6]);
} else if(action == "remove" || action == "uninstall" || action == "delete"){
let status = await pluginManager.removePlugin(process.argv[6]);
if(status && status.msg) console.log(status.msg);
} else if(action == "enable"){
let status = await pluginManager.enablePlugin(process.argv[6]);
if(status && status.msg) console.log(status.msg);
} else if(action == "disable"){
let status = await pluginManager.disablePlugin(process.argv[6]);
if(status && status.msg) console.log(status.msg);
}
process.exit(0);
}catch(e){
console.error(e);
process.exit(1);
}})();
} else {
usage(instance);
}
} else {
console.log('Usage:');
usage(instance);
}
// process.exit(0);
} else if (command == "delete") {
if (!process.argv[3]) {
console.error("Usage: node client.js delete [instance]");
process.exit(1);
} else if (typeof process.argv[3] == "string" && fs.existsSync(config.instanceDirectory+"/" + process.argv[3]) && process.argv[3] != "/" && process.argv[3] != "") {
fileOps.deleteFolderRecursiveSync(path.resolve(config.instanceDirectory, process.argv[3])); // TODO: Check if this can cause i-craft users to format their server by using wrong paths
console.log("Deleted instance " + process.argv[3]);
process.exit(0);
} else {
console.error("Instance not found: " + process.argv[3]);
process.exit(0);
}
} else if (command == "download") {
console.log("Downloading mods...");
// get JSON data about releases
let res = syncRequest('GET', 'https://api.github.com/repos/Danielv123/factorioClusterioMod/releases', {"headers":{"User-Agent":"Fuck you for requiring user agents!"}});
let url = JSON.parse(res.getBody())[0].assets[0].browser_download_url;
let name = JSON.parse(res.getBody())[0].assets[0].name;
if(url) {
console.log(url);
let file = fs.createWriteStream("sharedMods/"+name);
https.get(url, function(response) {
response.on('end', function () {
console.log("Downloaded "+name);
process.exit(0);
});
response.pipe(file);
}).end();
}
} else if (command == "start" && instance === undefined) {
console.error("ERROR: No instanceName provided!");
console.error("Usage: node client.js start [instanceName]");
process.exit(0);
} else if (command == "start" && typeof instance == "string" && instance != "/" && !fs.existsSync(instancedirectory)) {
// if instance does not exist, create it
console.log("Creating instance...");
fs.mkdirSync(instancedirectory);
fs.mkdirSync(instancedirectory + "/script-output/");
fs.mkdirSync(instancedirectory + "/saves/");
fs.writeFileSync(instancedirectory + "/script-output/output.txt", "");
fs.writeFileSync(instancedirectory + "/script-output/orders.txt", "");
fs.mkdirSync(instancedirectory + "/mods/");
fs.mkdirSync(instancedirectory + "/instanceMods/");
fs.mkdirSync(instancedirectory + "/scenarios/");
ncp("lib/scenarios", path.resolve(instancedirectory, "scenarios"), err => {
if (err) console.error(err)
});
// fs.symlinkSync('../../../sharedMods', instancedirectory + "/mods", 'junction') // This is broken because it can only take a file as first argument, not a folder
fs.writeFileSync(instancedirectory + `/config.ini`, `[path]\r\n
read-data=${ path.resolve(config.factorioDirectory, "data") }\r\n
write-data=${ path.resolve(config.instanceDirectory, instance) }\r\n
`);
// this line is probably not needed anymore but Im not gonna remove it
fs.copySync('sharedMods', path.join(instancedirectory, "mods"));
let instconf = {
"factorioPort": args.port || process.env.FACTORIOPORT || Math.floor(Math.random() * 65535),
"clientPort": args["rcon-port"] || process.env.RCONPORT || Math.floor(Math.random() * 65535),
"__comment_clientPassword": "This is the rcon password. Its also used for making an instanceID. Make sure its unique and not blank.",
"clientPassword": args["rcon-password"] || Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 8),
"info": {}
}
console.log("Clusterio | Created instance with settings:")
console.log(instconf);
// create instance config
fs.writeFileSync(instancedirectory + "/config.json", JSON.stringify(instconf, null, 4));
let name = "Clusterio instance: " + instance;
if (config.username) {
name = config.username + "'s clusterio " + instance;
}
let serversettings = {
"name": name,
"description": config.description,
"tags": ["clusterio"],
"max_players": "20",
"visibility": config.visibility,
"username": config.username,
"token": config.token,
"game_password": config.game_password,
"verify_user_identity": config.verify_user_identity,
"admins": [config.username],
"allow_commands": config.allow_commands,
"autosave_interval": 10,
"autosave_slots": 5,
"afk_autokick_interval": 0,
"auto_pause": config.auto_pause,
};
fs.writeFileSync(instancedirectory + "/server-settings.json", JSON.stringify(serversettings, null, 4));
console.log("Server settings: "+JSON.stringify(serversettings, null, 4));
console.log("Creating save .....");
let factorio = child_process.spawn(
'./' + config.factorioDirectory + '/bin/x64/factorio', [
'-c', instancedirectory + '/config.ini',
// '--create', instancedirectory + '/saves/save.zip',
'--start-server-load-scenario', 'Hotpatch',
'--server-settings', instancedirectory + '/server-settings.json',
'--rcon-port', Number(process.env.RCONPORT) || instconf.clientPort,
'--rcon-password', instconf.clientPassword,
], {
'stdio': ['pipe', 'pipe', 'pipe'],
'detached': process.platform === "linux",
}
);
factorio.stdout.on("data", data => {
data = data.toString("utf8").replace(/(\r\n\t|\n|\r\t)/gm,"");
console.log(data);
if(data.includes("Starting RCON interface")){
let client = new Rcon({
packetResponseTimeout: 200000,
maxPending: 5
});
client.connect({
host: 'localhost',
port: Number(process.env.RCONPORT) || instconf.clientPort,
password: instconf.clientPassword
});
client.onDidAuthenticate(() => {
console.log('Clusterio | RCON Authenticated!');
});
client.onDidConnect(() => {
console.log('Clusterio | RCON Connected, starting save');
client.send("/c game.server_save('hotpachSave')");
});
}
if(data.includes("Saving finished")){
console.log("Map saved as hotpachSave.zip, exiting...");
console.log("Instance created!")
process.exit(0);
}
if(data.includes("Downloading from auth server failed")){
console.error("Instance creation failed, unable to establish auth server connection.");
console.error("Deleting broken instance...");
fileOps.deleteFolderRecursiveSync(path.resolve(config.instanceDirectory, process.argv[3]));
process.exit(0);
}
});
} else if (command == "start" && typeof instance == "string" && instance != "/" && fs.existsSync(instancedirectory)){(async () => {
// Exit if no instance specified (it should be, just a safeguard);
if(instancedirectory != config.instanceDirectory+"/undefined"){
var instanceconfig = require(path.resolve(instancedirectory,'config'));
instanceconfig.unique = stringUtils.hashCode(instanceconfig.clientPassword);
if(process.env.FACTORIOPORT){
instanceconfig.factorioPort = process.env.FACTORIOPORT;
}
if(process.env.RCONPORT){
instanceconfig.rconPort = process.env.RCONPORT;
}
} else {
process.exit(1);
}
console.log("Deleting .tmp.zip files");
let savefiles = fs.readdirSync(path.join(instancedirectory,"saves"));
for(i = 0; i < savefiles.length; i++){
if(savefiles[i].substr(savefiles[i].length - 8, 8) == ".tmp.zip") {
fs.unlinkSync(path.resolve(instancedirectory, "saves", savefiles[i]));
}
}
console.log("Clusterio | Rotating old logs...");
// clean old log file to avoid crash
try{
let logPath = path.join(instancedirectory,'factorio-current.log');
let stat = await fs.stat(logPath);
console.log(stat)
console.log(stat.isFile())
if(stat.isFile()){
let logFilename = `factorio-${Math.floor(Date.parse(stat.mtime)/1000)}.log`;
await fs.rename(logPath, path.join(instancedirectory, logFilename));
console.log(`Log rotated as ${logFilename}`);
}
}catch(e){}
// Math.floor(Date.now()/1000)
// (new Date).toGMTString()
// move mods from ./sharedMods to the instances mod directory
try{fs.mkdirSync(path.join(instancedirectory, "instanceMods"));}catch(e){}
try{rmdirSync(path.join(instancedirectory, "mods"));}catch(e){}
try {
// mods directory that will be emptied (deleted) when closing the server to facilitate seperation of instanceMods and sharedMods
fs.mkdirSync(path.join(instancedirectory, "mods"));
} catch(e){}
console.log("Clusterio | Moving shared mods from sharedMods/ to instance/mods...");
fs.copySync('sharedMods', path.join(instancedirectory, "mods"));
console.log("Clusterio | Moving instance specific mods from instance/instanceMods to instance/mods...");
fs.copySync(path.join(instancedirectory, "instanceMods"), path.join(instancedirectory, "mods"));
// Set paths for factorio so it reads and writes from the correct place even if the instance is imported from somewhere else
fs.writeFileSync(instancedirectory + `/config.ini`, `[path]\r\n
read-data=${ path.resolve(config.factorioDirectory, "data") }\r\n
write-data=${ path.resolve(config.instanceDirectory, instance) }\r\n
`);
// Spawn factorio server
//var serverprocess = child_process.exec(commandline);
fileOps.getNewestFile(instancedirectory + "/saves/", fs.readdirSync(instancedirectory + "/saves/"),function(err, latestSave) {
if(err) {
console.error("ERROR!");
console.error("Your savefile seems to be missing. This might because you created an instance without having factorio\
installed and configured properly. Try installing factorio and adding your savefile to instances/[instancename]/saves/\n");
throw err;
}
// implicit global
serverprocess = child_process.spawn(
'./' + config.factorioDirectory + '/bin/x64/factorio', [
'-c', instancedirectory + '/config.ini',
'--start-server', latestSave.file,
'--rcon-port', args["rcon-port"] || Number(process.env.RCONPORT) || instanceconfig.clientPort,
'--rcon-password', args["rcon-password"] || instanceconfig.clientPassword,
'--server-settings', instancedirectory + '/server-settings.json',
'--port', args.port || Number(process.env.FACTORIOPORT) || instanceconfig.factorioPort
], {
'stdio': ['pipe', 'pipe', 'pipe'],
'detached': process.platform === "linux",
}
);
serverprocess.on('close', code => {
console.log(`child process exited with code ${code}`);
process.exit();
});
serverprocess.stdout.on("data", data => {
// log("Stdout: " + data);
if(data.toString('utf8').includes("Couldn't parse RCON data: Maximum payload size exceeded")){
console.error("ERROR: RCON CONNECTION BROKE DUE TO TOO LARGE PACKET!");
console.error("Attempting reconnect...");
client.disconnect();
client.connect();
}
// we have to do this to make logs visible on linux and in powershell. Causes log duplication for people with CMD.
console.log('Fact: ' + data.toString("utf8").replace("\n", ""));
});
serverprocess.stderr.on('data', (chunk) => {
console.log('ERR: ' + chunk);
});
// connect to the server with rcon
if(true || process.platform != "linux"){
// IP, port, password
client = new Rcon({
packetResponseTimeout: 200000, // 200s, should allow for commands up to 1250kB in length
maxPending: 5
});
// check the logfile to see if the RCON interface is running as there is no way to continue without it
// we read the log every 2 seconds and stop looping when we start connecting to factorio
function checkRcon() {
fs.readFile(instancedirectory+"/factorio-current.log", function (err, data) {
// if (err) console.error(err);
if(data && data.indexOf('Starting RCON interface') > 0){
client.connect({
host: 'localhost',
port: args["rcon-port"] || Number(process.env.RCONPORT) || instanceconfig.clientPort,
password: args["rcon-password"] || instanceconfig.clientPassword,
});
} else {
setTimeout(function(){
checkRcon();
},5000);
}
});
}
setTimeout(checkRcon, 5000);
client.onDidAuthenticate(() => {
console.log('Clusterio | RCON Authenticated!');
instanceManagement(instanceconfig); // start using rcons
});
client.onDidConnect(() => {
console.log('Clusterio | RCON Connected!');
// getID();
});
client.onDidDisconnect(() => {
console.log('Clusterio | RCON Disconnected!');
// process.exit(0); // exit because RCON disconnecting is undefined behaviour and we rather just wanna restart now
});
process.on('SIGINT', function () {
console.log("Caught interrupt signal, disconnecting RCON");
client.disconnect().then(()=>{
console.log("Rcon disconnected, Sending ^C");
// We don't actually do this on windows, because ctrl+c in windows CMD sends it to all subprocesses as well. Doing it twice will abort factorios save.
if(process.platform == "linux"){
serverprocess.kill("SIGINT");
}
});
});
} else if(process.platform == "linux"){
// don't open an RCON connection and just use stdio instead, does not work on windows.
instanceManagement(instanceconfig);
process.on('SIGINT', function () {
console.log("Caught interrupt signal, sending ^C");
serverprocess.kill("SIGINT");
});
}
// set some globals
confirmedOrders = [];
lastSignalCheck = Date.now();
});
})()} else {
console.error("Invalid arguments, quitting.");
process.exit(1);
}
// ensure instancemanagement only ever runs once
var _instanceInitialized;
async function instanceManagement(instanceconfig) {
if (_instanceInitialized) return;
_instanceInitialized = true;
console.log("Started instanceManagement();");
/* Open websocket connection to master */
var socket = ioClient("http://"+config.masterIP+":"+config.masterPort+"?token="+config.masterAuthToken);
socket.on("error", err => {
console.log("SOCKET | Error: ", err);
});
socket.on("hello", data => {
console.log("SOCKET | registering slave!");
socket.emit("registerSlave", {
instanceID: instanceconfig.unique,
});
});
setInterval(B=> socket.emit("heartbeat"), 10000);
// load plugins and execute onLoad event
const pluginManager = require("lib/manager/pluginManager.js")(config);
let pluginsToLoad = await pluginManager.getPlugins();
let plugins = [];
for(let i = 0; i < pluginsToLoad.length; i++){
let pluginLoadStarted = Date.now();
let combinedConfig = deepmerge(instanceconfig,config,{clone:true});
let pluginConfig = pluginsToLoad[i];
if(!global.subscribedFiles) {
global.subscribedFiles = {};
}
if(pluginConfig.binary == "nodePackage" && pluginConfig.enabled){
// require plugin class and execute it
let pluginClass = require(path.resolve(pluginConfig.pluginPath, "index.js"));
plugins[i] = new pluginClass(combinedConfig, async function(data, callback){
if(data && data.toString('utf8')[0] != "/") {
console.log("Clusterio | "+ pluginsToLoad[i].name + " | " + data.toString('utf8'));
return true;
} else if (data && data.toString('utf8')[0] == "/"){
return messageInterface(data.toString('utf8'), callback);
}
}, { // extra functions to pass in object. Should have done it like this from the start, but won't break backwards compat.
socket, // socket.io connection to master (and ES6 destructuring, yay)
});
if(plugins[i].factorioOutput && typeof plugins[i].factorioOutput === "function"){
// when factorio logs a line, send it to the plugin. This includes things like autosaves, chat, errors etc
serverprocess.stdout.on("data", data => plugins[i].factorioOutput(data.toString()));
}
if(pluginConfig.scriptOutputFileSubscription && typeof pluginConfig.scriptOutputFileSubscription == "string"){
if(global.subscribedFiles[pluginConfig.scriptOutputFileSubscription]) {
// please choose a unique file to subscribe to. If you need plugins to share this interface, set up a direct communication
// between those plugins instead.
throw "FATAL ERROR IN " + pluginConfig.name + " FILE ALREADY SUBSCRIBED " + pluginConfig.scriptOutputFileSubscription;
}
if (!fs.existsSync(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription)) {
// Do something
fs.writeFileSync(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription, "");
}
global.subscribedFiles[pluginConfig.scriptOutputFileSubscription] = true;
console.log("Clusterio | Registered file subscription on "+instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription);
if(!pluginConfig.fileReadDelay || pluginConfig.fileReadDelay == 0) {
// only wipe the file on restart for now, should most likely be rotated during runtime too
fs.writeFileSync(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription, "");
let tail = new Tail(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription);
tail.on("line", function (data) {
plugins[i].scriptOutput(data);
});
} else {
fs.watch(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription, fileChangeHandler);
// run once in case a plugin wrote out information before the plugin loaded fully
// delay, so the socket got enough time to connect
setTimeout(() => {
fileChangeHandler(false, pluginConfig.scriptOutputFileSubscription);
}, 500);
// send file contents to plugin for processing
function fileChangeHandler(eventType, filename) {
if (filename != null) {
setTimeout(
() => {
// get array of lines in file
let stuff = fs.readFileSync(instancedirectory + "/script-output/" + filename, "utf8").split("\n");
// if you found anything, reset the file
if (stuff[0]) {
fs.writeFileSync(instancedirectory + "/script-output/" + filename, "");
}
for (let o = 0; o < stuff.length; o++) {
if (stuff[o] && !stuff[o].includes('\u0000\u0000')) {
try {
plugins[i].scriptOutput(stuff[o]);
} catch (e) {
console.error(e)
}
}
}
},
pluginConfig.fileReadDelay || 0
);
}
}
}
}
console.log(`Clusterio | Loaded plugin ${pluginsToLoad[i].name} in ${Date.now() - pluginLoadStarted}ms`);
} else {
// this plugin doesn't have a client portion. Maybe it runs on the master only?
}
}
// world IDs ------------------------------------------------------------------
hashMods(instance, function(modHashes){
setInterval(getID, 10000);
getID();
function getID() {
messageInterface("/silent-command rcon.print(#game.connected_players)", function(playerCount) {
var payload = {
time: Date.now(),
rconPort: instanceconfig.clientPort,
rconPassword: instanceconfig.clientPassword,
serverPort: instanceconfig.factorioPort,
unique: instanceconfig.unique,
publicIP: config.publicIP, // IP of the server should be global for all instances, so we pull that straight from the config
mods:modHashes,
instanceName: instance,
info: instanceconfig.info,
};
if(playerCount){
payload.playerCount = playerCount.replace(/(\r\n\t|\n|\r\t)/gm, "");
} else {
payload.playerCount = 0;
}
function callback(err, mac) {
if (err) {
mac = "unknown";
console.log("##### getMac crashed, but we don't really give a shit because we are probably closing down #####");
}
global.mac = mac;
payload.mac = mac;
// console.log("Registered our presence with master "+config.masterIP+" at " + payload.time);
needle.post(config.masterIP + ":" + config.masterPort + '/api/getID', payload, needleOptionsWithTokenAuthHeader, function (err, response, body) {
if (err && err.code != "ECONNRESET"){
console.error("We got problems, something went wrong when contacting master"+config.masterIP+" at " + payload.time);
console.error(err);
} else if (response && response.body) {
// In the future we might be interested in whether or not we actually manage to send it, but honestly I don't care.
if (typeof response.body !== "string" || response.body.toLowerCase() !== "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) {
if (config.uploadModsToMaster) {
// [{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;
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("Bypassing sending mods to master because config.uploadModsToMaster is not enabled")
}
}
} // END OF INSTANCE START ---------------------------------------------------------------------
// string, function
// returns [{modName:string,hash:string}, ... ]
function hashMods(instanceName, callback) {
if(!callback) {
throw new Error("ERROR in function hashMods NO CALLBACK");
}
let modsDir = config.instanceDirectory+"/"+instanceName+"/mods/";
function hashMod(name) {
if (path.extname(name) != ".zip") {
// Can't hash unzipped mods, return null that's filtered out later
return null;
} else {
return hashFile(modsDir+name).then(hash => (
{modName: name, hash: hash}
));
}
}
let promises = fs.readdirSync(modsDir).map(hashMod);
Promise.all(promises).then(hashes => {
// Remove null entries from hashMod
callback(hashes.filter(entry => entry !== null));
});
}
-63
View File
@@ -1,63 +0,0 @@
{
"__comment": "Where factorio folder. Should be in the same folder as this config file.",
"factorioDirectory": "./factorio",
"instanceDirectory": "./instances",
"databaseDirectory": "./database",
"__comment1": "IP players should connect to to join your servers, should be portforwarded and stuff",
"publicIP": "localhost",
"__comment2": "IP the master web server runs at, can be ignored on the master system",
"masterIP": "localhost",
"__commentMasterPort": "HTTP port for the master to listen to",
"masterPort": 8080,
"__commentsslPort": "HTTPS port for the master to listen to. Set to 0 or an empty string to not listen for HTTPS.",
"sslPort": 0,
"sslCert": "database/certificates/cert.crt",
"sslPrivKey": "database/certificates/cert.key",
"__commentMasterAuthToken": "Authentication token to connect to the master server. Find it in secret-api-token.txt on the master.",
"masterAuthToken": "",
"__commentAuthSecret": "This is a long random string used for generating authentication tokens. Do not share. Regenerates when secret-api-token.txt is deleted.",
"masterAuthSecret": "",
"__comment3": "Your factorio.com username for factorio MP services, also username will be admin on new instances",
"username": "",
"__comment4": "Token from your factorio.com profile page. Required for MP and mod manager services.",
"token": "",
"__comment7": "Used for communication with the mod portal, not needed if you manage mods by hand",
"factorio_version": "0.16.41",
"__comment5": "General user settings",
"verify_user_identity": true,
"auto_pause": false,
"allow_commands": "admins-only",
"game_password": "",
"description": "This server runs Clusterio",
"visibility": {
"public": true,
"lan": true
},
"__comment6": "Statistic gathering statistics, may affect server performance (master memory usage especially)",
"itemStats": {
"maxEntries":60,
"entriesPerSecond": 0.01666666667
},
"logItemTransfers": false,
"__comment6.5": "When the master is undersupplied, we have 3 different modes to 'fairly' distribute items. First to ask gets all, dole division using weird formula or experimental neural net tries to make stuff as fair as possible. Feel free to play around.",
"disableFairItemDistribution": false,
"useNeuralNetDoleDivider": true,
"uploadModsToMaster": false,
"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
}
+20
View File
@@ -0,0 +1,20 @@
digraph "Client Connection Flow" {
node [shape=box];
graph [newrank=true];
start [label="Start"];
wait_hello [label="Wait for hello"];
new_session [label="New session"];
wait_ready [label="Wait for ready"];
connected [label="Connected"];
abort [label="Abort"];
lost_connection [label="Connection lost or\nunexpectedly closed\nwhile connecting" style="dotted"];
lost_connection -> start [label="Wait random delay"];
start -> wait_hello [label="Connect to /api/socket"];
wait_hello -> new_session [label="Receive hello"];
new_session -> wait_ready [label="Send register_*"];
wait_ready -> connected [label="Receive ready"];
wait_ready -> abort [label="Connection closed with 4003"];
}
+99
View File
@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.42.3 (0)
-->
<!-- Title: Client Connection Flow Pages: 1 -->
<svg width="371pt" height="496pt"
viewBox="0.00 0.00 371.00 496.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 492)">
<title>Client Connection Flow</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-492 367,-492 367,4 -4,4"/>
<!-- start -->
<g id="node1" class="node">
<title>start</title>
<polygon fill="none" stroke="black" points="135,-384 81,-384 81,-348 135,-348 135,-384"/>
<text text-anchor="middle" x="108" y="-362.3" font-family="Times,serif" font-size="14.00">Start</text>
</g>
<!-- wait_hello -->
<g id="node2" class="node">
<title>wait_hello</title>
<polygon fill="none" stroke="black" points="164.5,-297 51.5,-297 51.5,-261 164.5,-261 164.5,-297"/>
<text text-anchor="middle" x="108" y="-275.3" font-family="Times,serif" font-size="14.00">Wait for hello</text>
</g>
<!-- start&#45;&gt;wait_hello -->
<g id="edge2" class="edge">
<title>start&#45;&gt;wait_hello</title>
<path fill="none" stroke="black" d="M108,-347.8C108,-336.16 108,-320.55 108,-307.24"/>
<polygon fill="black" stroke="black" points="111.5,-307.18 108,-297.18 104.5,-307.18 111.5,-307.18"/>
<text text-anchor="middle" x="187.5" y="-318.8" font-family="Times,serif" font-size="14.00">Connect to /api/socket</text>
</g>
<!-- new_session -->
<g id="node3" class="node">
<title>new_session</title>
<polygon fill="none" stroke="black" points="161,-210 55,-210 55,-174 161,-174 161,-210"/>
<text text-anchor="middle" x="108" y="-188.3" font-family="Times,serif" font-size="14.00">New session</text>
</g>
<!-- wait_hello&#45;&gt;new_session -->
<g id="edge3" class="edge">
<title>wait_hello&#45;&gt;new_session</title>
<path fill="none" stroke="black" d="M108,-260.8C108,-249.16 108,-233.55 108,-220.24"/>
<polygon fill="black" stroke="black" points="111.5,-220.18 108,-210.18 104.5,-220.18 111.5,-220.18"/>
<text text-anchor="middle" x="155.5" y="-231.8" font-family="Times,serif" font-size="14.00">Receive hello</text>
</g>
<!-- wait_ready -->
<g id="node4" class="node">
<title>wait_ready</title>
<polygon fill="none" stroke="black" points="167.5,-123 48.5,-123 48.5,-87 167.5,-87 167.5,-123"/>
<text text-anchor="middle" x="108" y="-101.3" font-family="Times,serif" font-size="14.00">Wait for ready</text>
</g>
<!-- new_session&#45;&gt;wait_ready -->
<g id="edge4" class="edge">
<title>new_session&#45;&gt;wait_ready</title>
<path fill="none" stroke="black" d="M108,-173.8C108,-162.16 108,-146.55 108,-133.24"/>
<polygon fill="black" stroke="black" points="111.5,-133.18 108,-123.18 104.5,-133.18 111.5,-133.18"/>
<text text-anchor="middle" x="164" y="-144.8" font-family="Times,serif" font-size="14.00">Send register_*</text>
</g>
<!-- connected -->
<g id="node5" class="node">
<title>connected</title>
<polygon fill="none" stroke="black" points="92,-36 0,-36 0,0 92,0 92,-36"/>
<text text-anchor="middle" x="46" y="-14.3" font-family="Times,serif" font-size="14.00">Connected</text>
</g>
<!-- wait_ready&#45;&gt;connected -->
<g id="edge5" class="edge">
<title>wait_ready&#45;&gt;connected</title>
<path fill="none" stroke="black" d="M64.78,-86.86C57.71,-82.13 51.29,-76.25 47,-69 43.04,-62.32 41.73,-54.22 41.69,-46.49"/>
<polygon fill="black" stroke="black" points="45.19,-46.55 42.31,-36.36 38.21,-46.13 45.19,-46.55"/>
<text text-anchor="middle" x="97.5" y="-57.8" font-family="Times,serif" font-size="14.00">Receive ready</text>
</g>
<!-- abort -->
<g id="node6" class="node">
<title>abort</title>
<polygon fill="none" stroke="black" points="198,-36 142,-36 142,0 198,0 198,-36"/>
<text text-anchor="middle" x="170" y="-14.3" font-family="Times,serif" font-size="14.00">Abort</text>
</g>
<!-- wait_ready&#45;&gt;abort -->
<g id="edge6" class="edge">
<title>wait_ready&#45;&gt;abort</title>
<path fill="none" stroke="black" d="M131.28,-86.66C137.25,-81.45 143.3,-75.4 148,-69 153.19,-61.93 157.5,-53.45 160.88,-45.5"/>
<polygon fill="black" stroke="black" points="164.14,-46.78 164.56,-36.19 157.63,-44.21 164.14,-46.78"/>
<text text-anchor="middle" x="259.5" y="-57.8" font-family="Times,serif" font-size="14.00">Connection closed with 4003</text>
</g>
<!-- lost_connection -->
<g id="node7" class="node">
<title>lost_connection</title>
<polygon fill="none" stroke="black" stroke-dasharray="1,5" points="188.5,-488 27.5,-488 27.5,-435 188.5,-435 188.5,-488"/>
<text text-anchor="middle" x="108" y="-472.8" font-family="Times,serif" font-size="14.00">Connection lost or</text>
<text text-anchor="middle" x="108" y="-457.8" font-family="Times,serif" font-size="14.00">unexpectedly closed</text>
<text text-anchor="middle" x="108" y="-442.8" font-family="Times,serif" font-size="14.00">while connecting</text>
</g>
<!-- lost_connection&#45;&gt;start -->
<g id="edge1" class="edge">
<title>lost_connection&#45;&gt;start</title>
<path fill="none" stroke="black" d="M108,-434.95C108,-422.36 108,-407.17 108,-394.35"/>
<polygon fill="black" stroke="black" points="111.5,-394.24 108,-384.24 104.5,-394.24 111.5,-394.24"/>
<text text-anchor="middle" x="175.5" y="-405.8" font-family="Times,serif" font-size="14.00">Wait random delay</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

+4
View File
@@ -0,0 +1,4 @@
for x in $(dirname $0)/*.dot; do
echo $x
dot $x -Tsvg -o ${x/.dot/.svg}
done
+23
View File
@@ -0,0 +1,23 @@
digraph "Client Reconnection Flow" {
node [shape=box];
graph [newrank=true];
reconnect [label="Reconnect"];
wait_hello [label="Wait for hello"];
resume_session [label="Resume session"];
new_session [label="New session" style="dashed"];
wait_continue [label="Wait for continue"];
connected [label="Connected"];
lost_connection [label="Connection lost or\nunexpectedly closed\nwhile reconnecting" style="dotted"];
abort [label="Abort"];
{ rank="same"; wait_continue; new_session };
lost_connection -> reconnect [label="Wait random delay"];
reconnect -> wait_hello [label="Connect to /api/socket"];
wait_hello -> resume_session [label="Receive hello"];
resume_session -> wait_continue [label="Send resume"];
wait_continue -> abort [label="Connection closed with 4003"];
wait_continue -> new_session [label="Receive invalidate"];
wait_continue -> connected [label="Receive ready"];
}
+112
View File
@@ -0,0 +1,112 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.42.3 (0)
-->
<!-- Title: Client Reconnection Flow Pages: 1 -->
<svg width="441pt" height="496pt"
viewBox="0.00 0.00 441.00 496.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 492)">
<title>Client Reconnection Flow</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-492 437,-492 437,4 -4,4"/>
<!-- reconnect -->
<g id="node1" class="node">
<title>reconnect</title>
<polygon fill="none" stroke="black" points="153.5,-384 62.5,-384 62.5,-348 153.5,-348 153.5,-384"/>
<text text-anchor="middle" x="108" y="-362.3" font-family="Times,serif" font-size="14.00">Reconnect</text>
</g>
<!-- wait_hello -->
<g id="node2" class="node">
<title>wait_hello</title>
<polygon fill="none" stroke="black" points="164.5,-297 51.5,-297 51.5,-261 164.5,-261 164.5,-297"/>
<text text-anchor="middle" x="108" y="-275.3" font-family="Times,serif" font-size="14.00">Wait for hello</text>
</g>
<!-- reconnect&#45;&gt;wait_hello -->
<g id="edge2" class="edge">
<title>reconnect&#45;&gt;wait_hello</title>
<path fill="none" stroke="black" d="M108,-347.8C108,-336.16 108,-320.55 108,-307.24"/>
<polygon fill="black" stroke="black" points="111.5,-307.18 108,-297.18 104.5,-307.18 111.5,-307.18"/>
<text text-anchor="middle" x="187.5" y="-318.8" font-family="Times,serif" font-size="14.00">Connect to /api/socket</text>
</g>
<!-- resume_session -->
<g id="node3" class="node">
<title>resume_session</title>
<polygon fill="none" stroke="black" points="173.5,-210 42.5,-210 42.5,-174 173.5,-174 173.5,-210"/>
<text text-anchor="middle" x="108" y="-188.3" font-family="Times,serif" font-size="14.00">Resume session</text>
</g>
<!-- wait_hello&#45;&gt;resume_session -->
<g id="edge3" class="edge">
<title>wait_hello&#45;&gt;resume_session</title>
<path fill="none" stroke="black" d="M108,-260.8C108,-249.16 108,-233.55 108,-220.24"/>
<polygon fill="black" stroke="black" points="111.5,-220.18 108,-210.18 104.5,-220.18 111.5,-220.18"/>
<text text-anchor="middle" x="155.5" y="-231.8" font-family="Times,serif" font-size="14.00">Receive hello</text>
</g>
<!-- wait_continue -->
<g id="node5" class="node">
<title>wait_continue</title>
<polygon fill="none" stroke="black" points="178,-123 38,-123 38,-87 178,-87 178,-123"/>
<text text-anchor="middle" x="108" y="-101.3" font-family="Times,serif" font-size="14.00">Wait for continue</text>
</g>
<!-- resume_session&#45;&gt;wait_continue -->
<g id="edge4" class="edge">
<title>resume_session&#45;&gt;wait_continue</title>
<path fill="none" stroke="black" d="M108,-173.8C108,-162.16 108,-146.55 108,-133.24"/>
<polygon fill="black" stroke="black" points="111.5,-133.18 108,-123.18 104.5,-133.18 111.5,-133.18"/>
<text text-anchor="middle" x="155" y="-144.8" font-family="Times,serif" font-size="14.00">Send resume</text>
</g>
<!-- new_session -->
<g id="node4" class="node">
<title>new_session</title>
<polygon fill="none" stroke="black" stroke-dasharray="5,2" points="433,-123 327,-123 327,-87 433,-87 433,-123"/>
<text text-anchor="middle" x="380" y="-101.3" font-family="Times,serif" font-size="14.00">New session</text>
</g>
<!-- wait_continue&#45;&gt;new_session -->
<g id="edge6" class="edge">
<title>wait_continue&#45;&gt;new_session</title>
<path fill="none" stroke="black" d="M178.05,-105C220.94,-105 275.39,-105 316.94,-105"/>
<polygon fill="black" stroke="black" points="316.94,-108.5 326.94,-105 316.94,-101.5 316.94,-108.5"/>
<text text-anchor="middle" x="252.5" y="-111.8" font-family="Times,serif" font-size="14.00">Receive invalidate</text>
</g>
<!-- connected -->
<g id="node6" class="node">
<title>connected</title>
<polygon fill="none" stroke="black" points="92,-36 0,-36 0,0 92,0 92,-36"/>
<text text-anchor="middle" x="46" y="-14.3" font-family="Times,serif" font-size="14.00">Connected</text>
</g>
<!-- wait_continue&#45;&gt;connected -->
<g id="edge7" class="edge">
<title>wait_continue&#45;&gt;connected</title>
<path fill="none" stroke="black" d="M61.23,-86.99C53.95,-82.28 47.4,-76.37 43,-69 39.01,-62.33 38.18,-54.22 38.78,-46.49"/>
<polygon fill="black" stroke="black" points="42.27,-46.77 40.32,-36.36 35.35,-45.72 42.27,-46.77"/>
<text text-anchor="middle" x="93.5" y="-57.8" font-family="Times,serif" font-size="14.00">Receive ready</text>
</g>
<!-- abort -->
<g id="node8" class="node">
<title>abort</title>
<polygon fill="none" stroke="black" points="198,-36 142,-36 142,0 198,0 198,-36"/>
<text text-anchor="middle" x="170" y="-14.3" font-family="Times,serif" font-size="14.00">Abort</text>
</g>
<!-- wait_continue&#45;&gt;abort -->
<g id="edge5" class="edge">
<title>wait_continue&#45;&gt;abort</title>
<path fill="none" stroke="black" d="M131.28,-86.66C137.25,-81.45 143.3,-75.4 148,-69 153.19,-61.93 157.5,-53.45 160.88,-45.5"/>
<polygon fill="black" stroke="black" points="164.14,-46.78 164.56,-36.19 157.63,-44.21 164.14,-46.78"/>
<text text-anchor="middle" x="260.5" y="-57.8" font-family="Times,serif" font-size="14.00">Connection closed with 4003</text>
</g>
<!-- lost_connection -->
<g id="node7" class="node">
<title>lost_connection</title>
<polygon fill="none" stroke="black" stroke-dasharray="1,5" points="188.5,-488 27.5,-488 27.5,-435 188.5,-435 188.5,-488"/>
<text text-anchor="middle" x="108" y="-472.8" font-family="Times,serif" font-size="14.00">Connection lost or</text>
<text text-anchor="middle" x="108" y="-457.8" font-family="Times,serif" font-size="14.00">unexpectedly closed</text>
<text text-anchor="middle" x="108" y="-442.8" font-family="Times,serif" font-size="14.00">while reconnecting</text>
</g>
<!-- lost_connection&#45;&gt;reconnect -->
<g id="edge1" class="edge">
<title>lost_connection&#45;&gt;reconnect</title>
<path fill="none" stroke="black" d="M108,-434.95C108,-422.36 108,-407.17 108,-394.35"/>
<polygon fill="black" stroke="black" points="111.5,-394.24 108,-384.24 104.5,-394.24 111.5,-394.24"/>
<text text-anchor="middle" x="175.5" y="-405.8" font-family="Times,serif" font-size="14.00">Wait random delay</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

+21
View File
@@ -0,0 +1,21 @@
digraph "Server Connection Flow" {
node [shape=box];
graph [newrank=true];
new_connection [label="New connection"];
wait_handshake [label="Wait for handshake"];
validate_register [label="Validate register"];
validate_resume [label="Validate resume"];
connected [label="Connected"];
close [label="Close connection"];
new_connection -> wait_handshake [label="Send hello"];
wait_handshake -> validate_register [label="Receive register_*"];
wait_handshake -> validate_resume [label="Receive resume"];
wait_handshake -> close [label="Bad handshake\nTimeout"];
validate_register -> close [label="Bad token\nBad register"];
validate_register -> connected [label="Valid register:\nSend ready"];
validate_resume -> close [label="Bad token\nBad resume"];
validate_resume -> wait_handshake [label="Bad session:\nsend invalidate"];
validate_resume -> connected [label="Valid resume:\nSend continue"];
}
+118
View File
@@ -0,0 +1,118 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.42.3 (0)
-->
<!-- Title: Server Connection Flow Pages: 1 -->
<svg width="603pt" height="335pt"
viewBox="0.00 0.00 603.00 335.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 331)">
<title>Server Connection Flow</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-331 599,-331 599,4 -4,4"/>
<!-- new_connection -->
<g id="node1" class="node">
<title>new_connection</title>
<polygon fill="none" stroke="black" points="355.5,-327 224.5,-327 224.5,-291 355.5,-291 355.5,-327"/>
<text text-anchor="middle" x="290" y="-305.3" font-family="Times,serif" font-size="14.00">New connection</text>
</g>
<!-- wait_handshake -->
<g id="node2" class="node">
<title>wait_handshake</title>
<polygon fill="none" stroke="black" points="367.5,-240 212.5,-240 212.5,-204 367.5,-204 367.5,-240"/>
<text text-anchor="middle" x="290" y="-218.3" font-family="Times,serif" font-size="14.00">Wait for handshake</text>
</g>
<!-- new_connection&#45;&gt;wait_handshake -->
<g id="edge1" class="edge">
<title>new_connection&#45;&gt;wait_handshake</title>
<path fill="none" stroke="black" d="M290,-290.8C290,-279.16 290,-263.55 290,-250.24"/>
<polygon fill="black" stroke="black" points="293.5,-250.18 290,-240.18 286.5,-250.18 293.5,-250.18"/>
<text text-anchor="middle" x="328" y="-261.8" font-family="Times,serif" font-size="14.00">Send hello</text>
</g>
<!-- validate_register -->
<g id="node3" class="node">
<title>validate_register</title>
<polygon fill="none" stroke="black" points="136,-138 0,-138 0,-102 136,-102 136,-138"/>
<text text-anchor="middle" x="68" y="-116.3" font-family="Times,serif" font-size="14.00">Validate register</text>
</g>
<!-- wait_handshake&#45;&gt;validate_register -->
<g id="edge2" class="edge">
<title>wait_handshake&#45;&gt;validate_register</title>
<path fill="none" stroke="black" d="M212.45,-216.73C161.73,-212.32 101.57,-203.5 83,-186 72.82,-176.41 68.91,-161.41 67.62,-148.23"/>
<polygon fill="black" stroke="black" points="71.11,-147.99 67.07,-138.2 64.12,-148.37 71.11,-147.99"/>
<text text-anchor="middle" x="148.5" y="-167.3" font-family="Times,serif" font-size="14.00">Receive register_*</text>
</g>
<!-- validate_resume -->
<g id="node4" class="node">
<title>validate_resume</title>
<polygon fill="none" stroke="black" points="356,-138 224,-138 224,-102 356,-102 356,-138"/>
<text text-anchor="middle" x="290" y="-116.3" font-family="Times,serif" font-size="14.00">Validate resume</text>
</g>
<!-- wait_handshake&#45;&gt;validate_resume -->
<g id="edge3" class="edge">
<title>wait_handshake&#45;&gt;validate_resume</title>
<path fill="none" stroke="black" d="M241.15,-203.81C233.91,-199.14 227.42,-193.3 223,-186 216.09,-174.6 216.09,-167.4 223,-156 225.69,-151.55 229.16,-147.65 233.09,-144.22"/>
<polygon fill="black" stroke="black" points="235.23,-146.98 241.15,-138.19 231.04,-141.38 235.23,-146.98"/>
<text text-anchor="middle" x="280" y="-167.3" font-family="Times,serif" font-size="14.00">Receive resume</text>
</g>
<!-- close -->
<g id="node6" class="node">
<title>close</title>
<polygon fill="none" stroke="black" points="450.5,-36 313.5,-36 313.5,0 450.5,0 450.5,-36"/>
<text text-anchor="middle" x="382" y="-14.3" font-family="Times,serif" font-size="14.00">Close connection</text>
</g>
<!-- wait_handshake&#45;&gt;close -->
<g id="edge4" class="edge">
<title>wait_handshake&#45;&gt;close</title>
<path fill="none" stroke="black" d="M367.54,-213.17C403.99,-207.82 442.06,-199.24 454,-186 493.43,-142.29 497.55,-103.05 465,-54 461.92,-49.36 458.09,-45.33 453.78,-41.84"/>
<polygon fill="black" stroke="black" points="455.76,-38.95 445.55,-36.1 451.76,-44.69 455.76,-38.95"/>
<text text-anchor="middle" x="540.5" y="-123.8" font-family="Times,serif" font-size="14.00">Bad handshake</text>
<text text-anchor="middle" x="540.5" y="-108.8" font-family="Times,serif" font-size="14.00">Timeout</text>
</g>
<!-- connected -->
<g id="node5" class="node">
<title>connected</title>
<polygon fill="none" stroke="black" points="127,-36 35,-36 35,0 127,0 127,-36"/>
<text text-anchor="middle" x="81" y="-14.3" font-family="Times,serif" font-size="14.00">Connected</text>
</g>
<!-- validate_register&#45;&gt;connected -->
<g id="edge6" class="edge">
<title>validate_register&#45;&gt;connected</title>
<path fill="none" stroke="black" d="M34.78,-101.9C28.43,-97.02 22.64,-91.06 19,-84 12.9,-72.14 12.28,-65.51 19,-54 21.52,-49.69 24.78,-45.87 28.5,-42.49"/>
<polygon fill="black" stroke="black" points="30.9,-45.05 36.66,-36.16 26.61,-39.52 30.9,-45.05"/>
<text text-anchor="middle" x="70" y="-72.8" font-family="Times,serif" font-size="14.00">Valid register:</text>
<text text-anchor="middle" x="70" y="-57.8" font-family="Times,serif" font-size="14.00">Send ready</text>
</g>
<!-- validate_register&#45;&gt;close -->
<g id="edge5" class="edge">
<title>validate_register&#45;&gt;close</title>
<path fill="none" stroke="black" d="M136,-111.34C178.72,-105.47 229.03,-96.29 247,-84 260.03,-75.09 253.43,-63.55 266,-54 277.12,-45.55 290.27,-39.13 303.66,-34.26"/>
<polygon fill="black" stroke="black" points="305.06,-37.48 313.42,-30.97 302.83,-30.84 305.06,-37.48"/>
<text text-anchor="middle" x="311" y="-72.8" font-family="Times,serif" font-size="14.00">Bad token</text>
<text text-anchor="middle" x="311" y="-57.8" font-family="Times,serif" font-size="14.00">Bad register</text>
</g>
<!-- validate_resume&#45;&gt;wait_handshake -->
<g id="edge8" class="edge">
<title>validate_resume&#45;&gt;wait_handshake</title>
<path fill="none" stroke="black" d="M321.89,-138.33C327.98,-143.2 333.53,-149.09 337,-156 342.98,-167.92 342.98,-174.08 337,-186 335.05,-189.89 332.44,-193.45 329.44,-196.69"/>
<polygon fill="black" stroke="black" points="326.86,-194.32 321.89,-203.67 331.61,-199.46 326.86,-194.32"/>
<text text-anchor="middle" x="395.5" y="-174.8" font-family="Times,serif" font-size="14.00">Bad session:</text>
<text text-anchor="middle" x="395.5" y="-159.8" font-family="Times,serif" font-size="14.00">send invalidate</text>
</g>
<!-- validate_resume&#45;&gt;connected -->
<g id="edge9" class="edge">
<title>validate_resume&#45;&gt;connected</title>
<path fill="none" stroke="black" d="M223.7,-112.81C197.28,-107.96 167.61,-99.33 144,-84 129.91,-74.85 132.49,-66.26 121,-54 117.46,-50.23 113.56,-46.43 109.61,-42.79"/>
<polygon fill="black" stroke="black" points="111.86,-40.11 102.07,-36.06 107.2,-45.33 111.86,-40.11"/>
<text text-anchor="middle" x="195.5" y="-72.8" font-family="Times,serif" font-size="14.00">Valid resume:</text>
<text text-anchor="middle" x="195.5" y="-57.8" font-family="Times,serif" font-size="14.00">Send continue</text>
</g>
<!-- validate_resume&#45;&gt;close -->
<g id="edge7" class="edge">
<title>validate_resume&#45;&gt;close</title>
<path fill="none" stroke="black" d="M336.9,-101.98C345.46,-97.2 353.66,-91.27 360,-84 369.17,-73.47 374.56,-58.96 377.7,-46.32"/>
<polygon fill="black" stroke="black" points="381.18,-46.78 379.85,-36.27 374.33,-45.32 381.18,-46.78"/>
<text text-anchor="middle" x="418" y="-72.8" font-family="Times,serif" font-size="14.00">Bad token</text>
<text text-anchor="middle" x="418" y="-57.8" font-family="Times,serif" font-size="14.00">Bad resume</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.3 KiB

+59
View File
@@ -0,0 +1,59 @@
Communication Layer
===================
<sub>This document describes the communication layer in Clusterio and
unless you're developing Clusterio it's probably not going to be very
useful for you.</sub>
Communication is facilitated by the lib/link library and is based on
three core abstractions: links, connectors and messages. Links
represents one side of a two way communication pipe, connectors deal
with connecting and establishing communication, and messages are the
content sent over the link.
Link
----
The Link class represents one side of a two way communication channel
where the actual communication is done by a connector associated with
the link. The link also knows what the endpoint types it's connected to
are via the source and target attributes.
The endpoint types currently implemented are master, slave, instance,
and control. Although slave and instances run in the same Node.js
program they use virtual links to communicate between them in order to
simplify the communication architecture.
Upon creation the link will register with the connector in order to
receive and process messages from it. The messages received are
validated using the validator registered for the type of the message. A
message for which there is no validator for is considered an error.
Connector
---------
The connector used with a link is responsible for establishing and/or
maintain the connection that messages are sent over. Reconnect logic is
expected to be implemented in the connector, though this has not yet
been done.
There's also the virtual connector which lets two links in the same
program be connected to each other without having to establish a
network connection.
Message
-------
There's two types of messages currently implemented apart from the
Link builtin close message and that's Request and Event. The builtin
messages are defined in
[packages/lib/link/messages.js](/packages/lib/link/messages.js) and are
attached to links via the attachAllMessages function, which is usally
called in the Link subclass constructor.
The messages define which links they are valid on and only attaches
handlers for those links. There's also a forwarding mechanism for when
a request or event is to be forwarded another hop.
+102
View File
@@ -0,0 +1,102 @@
Configuration System
====================
<sub>This document describes the implementation and inner working of the
configuration system in Clusterio, and unless you're developing
Clusterio it's probably not going to be very useful for you.</sub>
Clusterio uses a group based configuration system to manage settings for
the master server, instances, slaves, and the clusterioctl utility.
Plugins get their own config group for the master server and instances
which they can add their own fields to. The fields are defined early in
the startup, and the groups and their fields for disabled plugins will
still be created.
The built-in config groups and their fields are defined in
[packages/lib/config/definitions.js](/packages/lib/config/definitions.js)
while plugins can add their own config groups to the `MasterConfigGroup`
and `InstanceConfigGroup` properties of the `info.js` export using
subclasses of `PluginConfigGroup`. The plugin group has the enabled
field pre-defined on it, and it's used to enable/disable plugins.
Defing Fields
-------------
Fields are defined using the `define` class method on group classes
after the groupName have been set and before the class has been
finalized. The `define` method takes an object as argument with the
following properties:
**type**:
The type of config value this field will support. The supported
values are boolean, string, number, and object, and if an attept is
made to set the field to a value that is not of the right type it
will be reject unless it's a string that can be converted to the
right type.
**name**:
Name of the configuration field, this is the primary way the field
is accessed, both in code and on the command line. As such it
should follow the lower\_case\_underscore style and not contain any
spaces or dots.
**title** (optional):
Text used to identify the config field in user interfaces.
**description** (optional):
Text used to describe the config field in user interfaces.
**enum** (optional):
Array of acceptable values this field can have. Attempts to set a
value not in this array will be rejected, which makes it useful for
making enumareted list of choices.
**optional** (optional):
True if this field can be set to null. Defaults to false.
**initial_value** (optional if optional is true):
Value this config field should take in a newly initialized config.
This can also be an async function returning the value to use.
If the value for a field stored in on disk somehow ends up being invalid
the field will take on the initial\_value instead when the config is
loaded.
Accessing Fields
----------------
On the Config instance there's two ways to access fields: through the
set and get methods using dot notation. E.g.:
let value = configInstance.get("group.field");
Or by getting the group first and then calling set or get on that.
E.g.:
let value = configInstance.group("group").get("field")
The set method takes an extra argument, which is the value to set the
field to. If the value is not valid this will throw an InvalidValue
exception, if the group or field doesn't exist it will throw an
InvalidField exception.
Config Lifecycle
----------------
Values for all config fields are created when the config is initialized,
as well as for any fields with missing values when a config is loaded.
Once a field has a value it is never automatically removed, even if the
field or group it was part of no longer exists. This ensures that
plugin configuration is not lost should the server be started up without
the plugins that defined the configuration installed.
For instance configs, they are kept on the master server and serialized
and sent over to the relevant slaves on startup. This makes it possible
to edit instance configs when the slave the instance is on is offline.
A copy is also stored in the instance directory on the slave, and this
config will be deliverd to the master server on slave startup, which
enables instances to be copied between clusters as long as their IDs are
unique.
+323
View File
@@ -0,0 +1,323 @@
Configuration
=============
Master Configuration
--------------------
### master.database_directory
Directory used to store cluster data to. Needs to be writeable by the
master server. This is also used by plugins to store its data.
Defaults to "database".
### master.http_port
Port to host HTTP server on. If set to null no HTTP server will be
exposed. At least one of this and master.https_port needs to be set to
a port.
Defaults to null.
### master.https_port
Port to host HTTPS server on. Uses master.tls_certificate and
master.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 master.http_port needs to be set to a port.
Defaults to 8443.
### master.external_address
External address the master server is accessible on. Currently only
used for `clusteriomaster 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.
### master.tls_certificate
TLS certificate to use for the HTTPS server when master.https_port is
configured. If neither this nor the configured master.tls_private_key
exists a self signed certificate is automatically created.
Defaults to "database/certificates/cert.crt".
### master.tls_private_key
TLS private key to use for the HTTPS server when master.https_port is
configured. If neither this nor the configured master.tls_certificate
exists a self signed certificate is automatically created.
Defaults to "database/certificates/cert.key".
### master.tls_bits
Number of bits to use when creating a self signed certificate.
Defaults to 2048.
### master.auth_secret
Base64 encoded authentication secret used to verify tokens issued to
slaves, 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.
### master.heartbeat_interval
Interval in seconds heartbeats are sent out at for WebSocket
connections. If a 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 60
### master.connector_shutdown_timeout
Timeout in seconds to use while shutting down the master server for link
sessions that currently don't have an active connection and haven't been
closed down properly. This happens when the network between a slave and
the master server goes down unexpectedly, or the slave goes down without
closing the connection properly either do to crashing or the machine
it's hosted on going down. If the timeout expires the session is
terminated and data loss may occur, if the slave went down this is
unavoidable.
Defaults to 30.
### master.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>.enabled
Whether to enable the given plugin on the master server. Plugins
disabled on the master server will not be loaded on instances.
Defaults to true.
Slave Configuration
-------------------
### slave.name
Name of the slave. Shows up in slave lists and is used to reference
this slave in the clusterioctl command line interface.
Defaults to "New Slave".
### slave.id
Imutable numeric id of the slave which uniquely identifies this slave in
the cluster.
Defaults to a random 31 bit number.
### slave.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".
### slave.instances_directory
Directory to store instances in. One sub-directory will be created in
it for each instance assigned to this slave and these sub-directories
will contain logs, saves, and various auto generated configuration files
for the instances.
Defaults to "instances".
### slave.master_url
URL to connect to the master server to.
Defaults to "https://localhost:8443/".
### slave.master_token
Access token used for authenticating with the master server. You can
generate an access token with `clusterioctl slave generate-token
--id <slave-id>`, or use the `clusterioctl slave create-config` to
create a new slave config with the correct url and token in it.
Defaults to "enter token here".
### slave.public_address
External address instances hosted on this slave 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".
### slave.reconnect_delay
Delay in seconds to wait after connection to the master server 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 slaves trying to reconnect at the same time.
Defaults to 5.
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_slave
Slave this instance is assigned to. To change this you need to use the
separate assign instance interface. After being assigned to a slave the
directory for the instance is created on the slave.
Defaults to null meaning not assigned.
### 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 slave.
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
### 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.
Defaults to {"tags":["clusterio"],"auto_pause":false}.
### 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
### <plugin_name>.enabled
Whether to enable the given plugin on the instance. Note that plugins
need to be enabled on the master server in order for them to be loaded
on instances.
Defaults to true
+264
View File
@@ -0,0 +1,264 @@
Contributing to Clusterio
=========================
You are probably reading this because you want to contribute code to
Clusterio. Great! We need people like you. Though we have some
conventions and workflows that we kindly ask you to follow.
Contents
--------
- [Workflow](#workflow)
- [Starting a Feature Branch](#starting-a-feature-branch)
- [Setting up the Development Environment](#setting-up-the-development-environment)
- [Checking your code for errors](#checking-your-code-for-errors)
- [Submitting a Pull Request](#submitting-a-pull-request)
- [Editing your Feature Branch](#editing-your-feature-branch)
- [Resolving Merge Conflicts](#resolving-merge-conflicts)
- [Updating Your GitHub Fork](#updating-your-github-fork)
- [Changelog](#changelog)
- [Supported Node.js and Factorio Version](#supported-node.js-and-factorio-version)
- [Code Style](#code-style)
- [Naming Style](#naming-style)
- [Indenting](#indenting)
- [Line Length](#line-length)
Workflow
--------
While we don't require you to follow this workflow, it makes things much
simpler for both you and us if you choose to do so. Especially when it
comes to making the commit history reflect what happened and not be a
giant ball of mess. GitHub's git workflow is less than ideal for
dealing with small feature-branches so you might see things being done
here a little diffrent than the usual.
It is recommended that you use the command line git version for doing
the operations described here. GUI wrappers tend to have different
ideas of what is what and you might find it difficult to make it do the
things described here. For doing the actual commits though, your
faviorit git GUI should work just fine.
If you pressed the fork button on GitHub and used `git clone` _on your
own fork_ you can skip to the next paragraph. Otherwise you will have
to click the fork button and clone your fork. If you already cloned the
main GitHub repository you can change your clone to point at your own
fork using the `git remote` command, but it's probably easier to delete
your local clone and clone it again.
To keep your local repository up to to date with the main repository add
the main repository as a remote.
git remote add upstream https://github.com/clusterio/factorioClusterio
After this running `git fetch upstream` will get the updated branches
from the main repository. Do not run `git pull upstream` it will mess
thing up!
### Setting up the Development Environment
The project is organized into several packages that are all managed
through Lerna. In order to get the development environment up and
running you will need to run the following commands:
npm install
npx lerna bootstrap
This installs dependencies needed by tests and links the packages up so
they work from the git work tree.
### Starting a Feature Branch
Development is done exclusively in feature branches. This means the
first thing to do before starting any development at all is to make a
new branch starting from the master or 1.2.x branch of the main
repository. To do this check out that branch from the main repository
(substitute master for 1.2.x if you're working on that branch.)
git fetch upstream
git checkout upstream/master
This will give a warning about detached HEAD which means you did it
correctly. Now you can make a feature branch based on this with.
git checkout -b my-feature-branch
The name of the branch is not important, as long as it's not a name
you've used before. But it's nice to have its name be somewhat
descriptive of what it implements.
### Checking your code for errors
There's automated tests and linting setup for the project. Please do
add tests for any code you add and run both:
npm run test
npm run lint
To check that your changes pass the integration tests as well as the
ESLint rules set up for the project.
### Submitting a Pull Request
Once you've commited changes to your feature branch that you want to
have included into the main repository push it to you fork with
git push origin my-feature-branch
and make a pull request through the GitHub web interface. You can also
pass the `--set-upstream` (or `-u`) flag to make `git push` do the same
thing for this branch. After your feature branch have been merged you
should consider it final. If there are more changes you want to have
merged or mistakes you need to fix you must start a new feature branch
and submit a new pull request
### Editing your Feature Branch
Until your feature branch is merged you can edit it as you see fit by
using the rebase tool. If your feature branch has been merged however
it's final and you will have to start a new feature branch and submit
that in a new pull request. The base usage of rebase assuming you are
checked out on your feature branch is the following command (substitute
master with 1.2.x if your feature branch is based on that)
git rebase -i upstream/master
This will open up an editor with the commits of your feature branch and
let you reorder, edit, and remove commits as you see fit.
If the main repository has been updated and you have fetched down those
updates with `git fetch upstream` this will also move your feature
branch so that it is based on the latest commit from the main
repository.
If you have already pushed your feature branch to your fork a simple
`git push origin my-feature-branch` will no longer work as the histories
are different, instead you will have to run
git push origin +my-feature-branch
The + sign signifies a force push and should be used with care. If you
have a pull request that has not yet been merged submitted for this
feature branch this will update the pull request too.
### Resolving Merge Conflicts
The best way to resolve merge conflicts in feature braches is to rebase
it on top of the updated upstream. Run the following commands
(substitute master with 1.2.x if your feature branch is based on that)
git fetch upstream
git rebase upstream/master
This will stop on the commit(s) that caused the merge conflict and let
you resolve them manually before continuing. Once you're done force
push your updated feature branch with
git push origin +my-feature-branch
### Updating Your GitHub Fork
No. This is pointless busywork that accomplishes nothing. Fetch the
main repository directly as described above and base your feature
branches on the refs from it. GitHub has no interface or functionality
for keeping the branches in your fork up to date with the branches in
the main repository and there is no point in doing this for our feature
branch based workflow.
Changelog
---------
There's a [Changelog](../CHANGELOG.md) in the root of the project. If
you make changes that are visible to users of this project you should
add an entry to the changelog describing what has changed. The format
of this log should be self explanatory, please follow it carefully.
Supported Node.js and Factorio Versions
---------------------------------------
Clusterio run on Node.js v12 and up, and v10 from v10.16.0, make sure
to check that the Node builtin API's and npm packages you use are
supported on these versions. The Travis CI tests are runned on whatever
version of Node.js v10 is the default, which at the time of writing is
v10.18.0.
For Factorio Clusterio 2.0 aims to support version 0.17.69 and up,
including the latest experimental release. It's recommended that you
use the lua API reference for 0.17.69, as there's no information on what
version Factorio API's were introduced in. The Travis CI tests are
runned against latest experimental release.
Code Style
----------
The style of the code is a bit of a mixed bag at the moment. But there
are at least a few things that have been agreed upon.
### Naming style
JavaScript variables in general uses camelCase. The exception is for
classes/constructors which use PascalCase and variables destructured
from over the wire objects.
Fields in configs and messages sent over links uses
lowercase_underscore.
JavaScript files are named using lowercase_underscore for files that
export multiple items. Files containing and exporting a single class
should be named the same as the class in PascalCase.
Imported modules from the lib package are prefixed with lib to make them
easier to distinguish, as the names tend to be very generic. E.g.:
const libLink = require("lib/link");
const libLuaTools = require("lib/lua_tools");
For lua code lowercase_underscore is used for everything.
### Strings
JavaScript and Lua code should use double quoted strings (`"string"`)
for all strings with the only exception that strings that contain double
quotes may use single quotes (i.e., `'String with "quotes"'`).
JavaScript template literals may also be used where appropriate.
### Indenting
JavaScript code should be indented with tabs.
Lua code should be indented with 4 spaces.
### Line Length
Lines are limited to 120 characters where tabs count as 4 characters.
If the argument list for a function or expressino for compound statement
gets very long break them out on their own line(s). For example:
function foo(
really, long, argument, list,
over, multiple, lines
) {
if (
really && long && expression
|| over && multiple && lines
) {
// code
}
})
+387
View File
@@ -0,0 +1,387 @@
Developing for Clusterio
========================
This document describes the various different types of content that can
be created for Factorio and/or Clusterio and how this is made compatible
with Clusterio.
Contents
--------
- [Factorio Mods](#factorio-mods)
- [Factorio Scenarios](#factorio-scenarios)
- [event_handler interface](#event_handler-interface)
- [Communicating with Clusterio](#communicating-with-clusterio)
- [Clusterio Modules](#clusterio-modules)
- [Clusterio Lua API](#clusterio-lua-api)
- [Clusterio Plugins](#clusterio-plugins)
Factorio Mods
-------------
Factorio mods that do not interact with Clusterio should not have any
considerations that need to be taken into account for them to work with
Clusterio.
Factorio Scenarios
------------------
Clusterio patches saves and because of this scenarios have a few
limitations and must explicitly mark themselves as compatible with
Clusterio to work. The main limitation is that the
[event_handler](https://github.com/wube/factorio-data/blob/master/core/lualib/event_handler.lua)
lib must be used, and control.lua cannot contain any code other than
calls to event_handler loading the relevant libs of the scenario.
This is because the control.lua file will be overwritten by the save
patcher. Scenarios also cannot use the event registering functions
exposed by `script` such as `script.on_event` or `script.on_nth_tick` as
using these will overwrite the handlers registered by the
event_handler lib.
A brief description of the usage of the event_handler library is
provided in the [event_handler interface](#event_handler-interface)
section.
Once a scenario is made using the event_handler it should have a
control.lua file that looks something like this:
```lua
-- control.lua
local event_handler = require("event_handler")
event_handler.add_lib(require("module_foo"))
event_handler.add_lib(require("module_bar"))
```
To make Clusterio recognize and correctly handle this scenario it
needs to include a clusterio.json file with following content:
```json
{
"scenario": {
"name": "example-scenario",
"modules": [
"module_foo",
"module_bar"
]
}
}
```
This tells Clusterio that it should load both module_foo and module_bar
by passing the result of requiring them to the `add_lib` function when
writing a new control.lua file to the save.
### event_handler interface
<sub>**Note:** At the time of writing the event_handler lib has been
a part of Factorio since at least 0.17.4, and was moved from base to
core in 0.17.63 but is still not documented anywhere. A brief
description is provided here for this reason.</sub>
The event_handler lib provides a simple interface for registering
multiple callbacks to the same events without having to be concerned
with callback chaining or overwriting callbacks defined elsewhere. It
works by taking over the task of registering the actual callbacks with
Factorio, providing its own interface for the rest of the code to use.
The sole function of interest exported by event_handler is `add_lib`
which accepts a table with event handler callbacks definitions that it
will register. The following entries are recognized by `add_lib`, all
of which are optional:
- `on_init`:
Callback called when the callback of `script.on_init` is invoked.
- `on_load`:
Callback called when the callback of `script.on_load` is invoked.
- `on_configuration_changed`:
Callback called when the callback of
`script.on_configuration_changed` is invoked.
- `events`:
Table mapping event ids as defined in `defines.events` to callbacks.
For example the table `{ [defines.events.on_player_died] = foo }`
will call bar with the usual `event` table as argument every time a
player dies.
- `on_nth_tick`:
Table mapping the nth tick number to a callback. For example the
table `{ [30] = bar }` will call bar every 30th tick.
- `add_remote_interface`:
Callback called before on_init/on_load callbacks<sup>[1]</sup>. It
has no special meaning and receives no arguments but should be used
for registering remote interfaces.
- `add_commands`:
Callback called before on_init/on_load callbacks<sup>[1]</sup>. It
has no special meaning and receives no arguments but should be used
for registering commands.
<sub>1: Before 0.17.69 these callbacks were called after
on_init/on_load.</sub>
The usual way to use `add_lib` is to define the table of events to
register in a separate file and return it, then load it in control.lua
via `require` before passing it to `add_lib`. For example a module
could be defined as
```lua
-- example.lua
local function my_handler(event)
local name = game.player[event.player_index].name
game.print("It appears that " .. name .. " has died")
end
return {
on_init = function() game.print("example_lib init") end,
events = {
[defines.events.on_player_died] = my_handler,
},
}
```
And then in control.lua the following used to load it with `add_lib`:
```lua
-- control.lua
local event_handler = require("event_handler")
event_handler.add_lib(require("example"))
```
Because the event_handler lib registers events itself you may not use
`script.on_load` or `script.on_init` at all in your code and any usage
of `script.on_configuration_changed`, `script.on_nth_tick` and/or
`script.on_event` will cause the corresponding events registered with
event_handler to break and should therefore not be used.
There's also the `add_libraries` function exported by event_handler,
which accepts a table and calls `add_lib` for each value in the table.
Communicating with Clusterio
----------------------------
There's a poorly to virtuarly undocumented HTTP interface that can be
used to communicate with Clusterio to do things like get the status of
instances, grab items from the store, run commands on instances, etc,
etc. For now you can read the source code, and maybe ask on the Discord
for some pointers if you get stuck.
Clusterio Modules
-----------------
Modules are primarily used by plugins to inject code into Factorio games
with the save patcher, though it's also possible to make stand alone
modules that are loaded into the game if you don't need the capabilites
of the plugin system. The save patcher puts modules into the `modules`
folder of the Factorio save and adds code to `control.lua` to load the
module according to the `load` and `require` options to the module.json
file.
Stand alone modules are placed into the modules folder of Clusterio,
plugin modules are located in the module folder of the plugin. In
either case a `module.json` file is required and has the following
structure:
```json
{
"name": "my_module",
"version": "1.2.0",
"dependencies": {
"clusterio": "*",
"foo": ">=0.4.2"
},
"require": ["bar.lua"],
"load": ["foo.lua"]
}
```
The following entries are supported in the module.json file:
- `name`:
Name of the module, must match the folder the module is located in
for stand alone modules or the name of the plugin the module is
located in.
- `version`:
Version of the module. Must be compatible with [Semantic Versioning
2.0.0](https://semver.org/). In plugin modules this defaults to the
version of the plugin otherwise it's required.
- `dependencies`:
Optional mapping of modules this module depends on and their
version. In the example `>=0.4.2` means that the foo module must be
at least version 0.4.2. See the [Ranges
syntax](https://www.npmjs.com/package/semver#ranges) of the
node-semver package for a full description of what operators are
supported. The events for dependencies is invoked before the events
of the dependent, and starting an instance will fail if the
dependencies cannot be satisfied. If not specified it will default
to depending on `clusterio`. Make sure to add `clusterio` to your
dependencies if you add other dependencies and depend on the Lua API.
- `require`:
Lua files to require in `control.lua`. This should only really be
needed if you want to make a global function available for use in
Lua commands. The paths are relative to the module's own folder and
should be specified using forward slashes as directory sepparators
if it's located in a sub directory in the module.
- `load`:
Lua files to load with the `event_handler` lib, the result of
requiring the file will be passed to the `add_lib` function of the
`event_handler` lib in `control.lua`. See the section on the
[event_handler interface](#event_handler-interface) for a detailed
description on how the `event_handler` lib works. The paths are
relative to the module's own folder and should be specified using
forward slashes as directory sepparators if it's located in a sub
directory in the module.
It's recommended to use the new style of defining modules in Lua, as
well as avoid the use of global variables as much as possible. This
means always declearing your top level variables and functions local and
exporting only the things you need in other files. You can require
other files in your module by prefixing your require paths with
`modules/your_module_name/`. It's also possible to require files from
other modules this way too.
The global variables and data stored in the global table is shared by
all Clusterio modules as well as the scenario, so it's important that
you use unique names. It's recommended to prefix the global variables
and data in the global table that your module has with the name of your
module.
Because modules can be patched into an existing game you cannot rely on
the `on_init` callback to be called in Clusterio Modules. Nor can you
rely on the `on_configuration_changed` callback, as this is not called
when level code changes. The Clusterio Lua API provides the custom
`on_server_startup` event that can be used as a substitute, see the next
section.
Clusterio Lua API
-----------------
Clusterio provides a save patched module as well as a regular Factorio
mod for interfacing with Clusterio. This includes tools sending data to
plugins, script events to listen for, and an item serialization module.
### clusterio_api library
**require path**
From a module
`local clusterio_api = require("modules/clusterio/api")`
From a mod
`local clusterio_api = require("__clusterio_lib__/api")`
Provides the main interface to Clusterio from within the game. When
using the mod version you must call `init` before it'll function
properly.
#### init
<sub>Mod version only</sub>
Initialize the API. This should be called from both the `on_init` and
`on_load` events in your mod, and before this is called certain features
will not be available, most notably the events table.
#### clusterio_api.events
Table of events raised by clusterio. If the game is started outside of
Clusterio then none of these events will be raised.
In the mod version this table does not exist before the `init` function
is called.
##### on_instance_updated
Raised after the name and id of an instance has been updated. This may
occur even if the id and name didn't change.
Event data:
- `instance_id`: the id of the instance.
- `instance_name`: the name of the instance.
##### on_server_startup
Raised when Clusterio is starting up the server. It can be used to
initialize clusterio related features in a mod, and as a stand-in for
the `on_init` and `on_configuration_changed` event for modules. It is
invoked on the first tick the server runs after the save has been
patched, before most other events.
Use this event to initialize and/or migrate the data structures
you need. Keep in mind that both mods and Clusterio modules can switch
from any version to any version so it should be able to handle both
forwards and backwards migrations gracefully.
#### clusterio_api.send_json(channel, data)
Send JSON data to Clusterio over the given channel. The `data` argument
should be table that can be serialized with `game.table_to_json`.
Clusterio plugins can listen to channels and will receive an event with
the data sent here. See the [Communicating with Factorio
section](writing-plugins.md#communicating-with-factorio) in the Writing
Plugins document for more information.
If the game is started outside of Clusterio the data will be sent, but
since there's no code following stdout to pick it up it will be lost.
**Note**: Payloads greater than 4 MB will cause stuttering in the game.
**Note**: This is not a binary safe way of sending data. Strings
embedded into the tables sent must be valid UTF-8 text.
Parameters:
- `channel`: string identifying which channel to send it on.
- `data`: table that can be converted to JSON with game.table_to_json
### serialize library
**require path**
From a module
`local serialize = require("modules/clusterio/serialize")`
From a mod
`local serialize = require("__clusterio_lib__/serialize")`
Serializes inventories and item stacks into a table that in turn can be
converted into JSON with `game.table_to_json`. Properly handles the
extra data for most items.
#### serialize.serialize_equipment_grid(LuaEquipmentGrid)
Returns a table containing a serialized representation of the equipment
grid passed.
#### serialize.deserialize_equipment_grid(LuaEquipmentGrid, serialized_grid)
Deserialize a previously serialized equipment grid into the target
LuaEquipmentGrid. Overwrites all content in the target.
#### serialize.serialize_item_stack(LuaItemStack, destination_table)
Serialize the LuaItemStack passed into the target destination_table.
The destination_table should be a new empty table.
#### serialize.deserialize_item_stack(LuaItemStack, serialized_stack)
Deserialize a previously serialized item stack into the target
LuaItemStack. Overwrites the content of the item stack.
#### serialize.serialize_inventory(LuaInventory)
Returns a table containing a serialized representation of the inventory
passed.
#### serialize.deserialize_inventory(LuaInventory, serialized_inventory)
Deserialize a previously serialized inventory into the target
LuaInventory. Overwrites slots that have content in the serialized
inventory.
Clusterio Plugins
-----------------
See the [Writing Plugins](writing-plugins.md) document for details on
the plugin interface and how to write plugins for Clusterio.
+30
View File
@@ -0,0 +1,30 @@
Save Patching
-------------
In order to efficiently get data in and out of running Factorio servers
and to provide custom interfaces Clusterio uses save
patching<sup>[1]</sup> to add code to the game. The save patching is
based on the built in
[event_handler](https://github.com/wube/factorio-data/blob/master/core/lualib/event_handler.lua)
library to Factorio and requires scenarios to be coded specifically for
it. Mods on the other hand work without modifications.
This can be disabled with the `factorio.enable_save_patching` config on
a per instance basis, however disabling it means that modules are not
loaded into the game and most plugins will not function.
<sub>1: Before version 2.0 of Clusterio this was done at runtime
with a patcher called Hotpatch, but due to the difficulty in supporting
run time patching and the lack of documentation it was replace it with
save patching instead.</sub>
User Accounts
-------------
The master server creates and stores accounts for all Factorio users
that log into instances under its control. These accounts are used to
store per player data and authorize management of the cluster. By
default users accounts are assigned a Player role that gives a limited
read access to the cluster resources, and admins of the cluster can be
assigned an Admin role to get access to manage it.
+23
View File
@@ -0,0 +1,23 @@
{
"plugins": [
"plugins/markdown"
],
"tags": {
"allowUnknownTags": false
},
"opts": {
"destination": "docs/api",
"recurse": true
},
"source": {
"include": [
"packages/master/src/",
"packages/master/master.js",
"packages/slave/slave.js",
"packages/ctl/ctl.js",
"packages/lib/",
"plugins/"
],
"excludePattern": "node_modules"
}
}
+132
View File
@@ -0,0 +1,132 @@
Managing a Cluster
==================
Clusterio clusters are managed through the master server by using the
`clusterioctl` command line interface which is invoked by running
`npx clusterioctl <command>` in the clusterio directory. This document
uses the shorthand `ctl> foo` to indicate `npx clusterioctl foo` should
be executed. Mandatory parameters are shown in `<angles bracket>` and
optional pameters are in `[square brackets]`.
Before `clusterioctl` can be used it needs to be configured for the
cluster it will connect to. The easiest way to do this is to run
`npx clusteriomaster bootstrap create-ctl-config <username>` on the
master server, which creates the necessary `config-control.json` for
managing the cluster as the given user.
Slaves
------
To be written.
Instances
---------
To be written.
Users
-----
Clusterio automatically creates user accounts for all players that join
an instance when save patching is enabled. These accounts are used to
store per player data shared between instances, like if the player
should be whitelisted, admin or banned.
### List users
ctl> user list
Lists all user accounts in the cluster along with some data for each.
### Create User
ctl> user create <name>
Creates a new empty user account for the given Factorio player. Note
that case matters here. This is usually not required as user accounts
are created automatically for players that join instances with save
patching enabled.
### Promote User to Server Admin
ctl> user set-admin <name> [--revoke] [--create]
Promotes the user given to in-game admin on instances with the
`sync_adminlist` option enabled. If the `--revoke` switch is used the
user is removed from the adminlist.
Since admin status is a part of the account data the account must exist
for this to succeed, passing `--create` will create the account if it
does not exist.
**Note:** Being a server admin does not grant any access to manage the
cluster. See [set-roles](#set-cluster-roles) for adding roles which
grant access to managing the clusetr.
### Whitelist User
ctl> user set-whitelisted <name> [--remove] [--create]
Add the user given to the whitelist on instances with the
`sync_whitelist` option enabled. If the `--remove` switch is used the
user is removed from the whitelist.
Since whitelisted status is a part of the account data the account must
exist for this to succeed, passing `--create` will create the account if
it does not exist.
### Ban User
ctl> user set-banned <name> [--reason <message>] [--pardon] [--create]
Ban user in-game from instances with the `sync_banlist` option enabled.
Reason is a message that will be shown to the user when attemption to
log in. If the `--pardon` switch is used it removes the ban.
Since ban status is a part of the account data the account must exist
for this to succeed, passing `--create` will create the account if it
does not exist.
Note: This bans the user from logging in to Factorio servers in the
cluster, it does not revoke access to any cluster management they might
have, see next section on setting roles for revoking tha.
### Set Cluster Roles
ctl> user set-roles <name> [roles...]
Replaces the roles the user has in the cluster with a new list of roles.
Calling with an empty roles argument will remove all roles from the
user.
By default there's a roled named Admin which grants access to
everything and a role named Player which grants a limited read access to
the cluster, see [the section on roles](#roles) for more information
about setting up roles and permissions.
### Delete user
ctl> user delete <name>
Deletes everything stored on the master server for this user.
Note: If the player is banned from the cluster this will effectively
unban them, as the ban status is stored with the user account.
Note: If the player joins the cluster again a new account will be made
for them automatically.
Roles
-----
To be written.
+287
View File
@@ -0,0 +1,287 @@
WebSocket protocol
==================
<sub>This document describes the protocol used between the master and
slaves, and unless you're developing Clusterio it's probably not going
to be very useful for you.</sub>
The protocol is built on WebSocket and uses JSON encoded text messages
to communicate to and from the master server. These messages contain
a JSON encoded object with the following three properties:
- seq - integer|null - The sequence number of the message
- type - string - The type of message
- data - object - Message data
For the handshake and heartbeats the seq property is null.
Handshake
---------
Upon connecting to the WebSocket endpoint the server will respond with a
`hello` message containing the master server version as the data. The
client is then expected to send either `register_*` or a `resume`
message to set up the connection with the master. The exact flow is
described below.
### Client connection flow
For a client making a new connecting to the master server the first
course of action after obtaining a token is to connect with WebSocket to
the /api/socket endpoint and wait for the server hello. After the
hello, the client sends a register\_\* message corresponding to the type
of the client. If successful the server will reply with a ready message
and the connection has been established. See the flowchart below.
![Client connection flowchart](assets/connection_flow.svg)
If for any reason the connection is closed or lost during the connection
attempt the process is started over after a random delay, with the
exception of the connection being closed with a 4003 code which means
authentication failed and the client should stop trying to connect. The
wait for hello and wait for ready states should also have a timeout,
after which the connection is closed if the hello/reply wasn't received
in time.
### Client reconnection flow
If the connection is lost after the client has received the ready
message from the server it should attempt a reconnect instead. This is
very similar to a new connection, but instead of a register\_\* messsage
the client sends a resume message and the server replies with a continue
message if resuming the session was successful. This continue message
has sequence number of the last message the master received and the
client is expected to imediately send all messages from its send buffer
that occured after the sequence number given.
The server will respond with an invalidate message if resuming the
session is not possible. In this case the client should continue from
the new session state of the connection flow (see the previous section).
![Client reconnection flowchart](assets/reconnection_flow.svg)
Like the connection flow the process starts over after a delay if the
connection is closed or lost. The same exception for connection closed
with 4003 applies as with the connection flow. Additionally after an
invalidate message has been received and the connection flow fails, it
should start over from the connection flow.
### Server connection flow
From the master server side new connections and reconnection attempts
start out the same, a new connection has been made and the server sends
the hello messoge to the client. If a vaild register\_\* message is
received a new connection is set up for the client and the ready message
is sent in reply. If a valid resume message is received and the session
that's attempted to be resumed is active it'll signal that the session
is resumed with a continue message and the client upon receiving the
continue message should send the messages from its send buffer from
after the last\_seq given in the continue message.
If the session token used to resume is not valid it'll send an invalidate
message and expect the client to continue with a registr\_\* message. If an
invalid or unexpected message is received during the handshake the connection
is closed. See the flowchart below.
![Client connection flowchart](assets/server_flow.svg)
For authentication failure the connection is closed with code 4003,
which the connection client should interpret as connecting will never
succeed and stop trying to reconnect.
### Connection tracking and heartbeats
Once connected as indicated by the ready and continue messages from the
master server the client must start sending heartbeats at the interval
given in the ready/continue message, and storing messages sent over the
link in a sending side buffer. The heartbeats sent must have the seq of
the last received message on the link and acts as an ack for received
messages. The master server also sends heartbeats in the same manner
and the client should remove messages up to and including the sequence
number received in the heartbeat from its send buffer.
Handshake messages
------------------
### `hello`
Sent by the master server when the WebSocket connection has been opened.
- version - string - The version of the master, e.g. "2.0.0".
- plugins - Object&lt;string, string&gt; -
Object mapping plugin names to plugin version for plugins that are
loaded on the master server.
### `register_slave`
Slave handshake for establishing a new connection session.
- token - string - Authentication token.
- agent - string - Human readable string of the software connecting.
- version - string - The protocol version of the slave, e.g. "2.0.0".
- name - string - Name of the slave.
- id - integer - ID of the slave.
- plugins - Object&lt;string, string&gt; -
Object mapping plugin names to plugin version for plugins that are
available on the slave.
### `register_control`
Control handshake for enstablishing a new connection session.
- token - string - Authentication token.
- agent - string - Human readable string of the software connecting.
- version - string - The protocol version of the control, e.g. "2.0.0".
### `resume`
Handshake for resuming an existing connection session where the
connection had dropped unexpectedly.
- session_token - string - Session token to resume session with.
- last_seq - integer|null - Last message seq received by the client.
### `invalidate`
Sent by the master server in reply to a resume for a session that cannot
be resumed. The client should start a new session with a register\_\*
message in response.
### `continue`
Sent by the master server to signal resuming the session was successful.
- last_seq - integer|null - Last message seq received by the server.
- heartbeat_interval - number - Rate client should send heartbeats at.
### `ready`
Sent by the master server to signal the connection has been established
with a new session.
- session_token - string - Token to resume the session with.
- heartbeat_interval - number - Rate client should send heartbeats at.
### `heartbeat`
Sent by both the master server and clients at the interval given in the
continue/ready messages.
- last_seq - integer - Last message seq received over the link.
Example handshake
-----------------
Server hello after opening the WebSocket.
```json
{
"seq": null,
"type": "hello",
"data": {
"version": "2.0.0"
}
}
```
Slave registering a new connection session.
```json
{
"seq": null,
"type": "register_slave",
"data": {
"token": "xyzabc123",
"agent": "Clusterio Slave",
"version": "2.0.0",
"name": "Foo",
"id": 123
}
}
```
Master server reply signaling the session is established and ready.
```json
{
"seq": null,
"type": "ready",
"data": {
"session_token": "xyz.azy.bzy",
"heartbeat_interval": 45
}
}
```
Events
------
Events are messages send in one direction over the link, that the
receiver is expected to act upon, but not make any replies (for that
there's [Requests](#requests). By convention message types ending in
`*_event` are events. The data payload of an event consists solely of
event specific properties.
See [packages/lib/link/messages.js](/packages/lib/link/messages.js) for
the recognized events and their contents.
Requests
--------
Requests are messages which expects a response in return. They function
a lot like HTTP requests except that both parties of a connection can
inniate a request that is expected to be responded to. By convention
message types ending in `*_request` are requests, and is expected to be
replied to with a corresponding `*_response` message. If an error
occured while processing the request an error response containing an
error property in the data payload is sent instead.
See [packages/lib/link/messages.js](/packages/lib/link/messages.js) for
the recognized requests and their contents.
### `*_response`
- seq - integer - The seq number of the request message.
- additional properties may be present depending on the type of request.
### `*_response` error
- seq - integer - The seq number of the request message.
- error - string - Human readable text message describing the error.
Graceful Disconnect
-------------------
To ensure no data is lost when closing the link it's important to
send a prepare disconnect request and wait for it to complete before
closing the WebSocket. Either party of the link can send this request
and the party sending the request should be the one that initates the
close of the WebSocket.
### `prepare_disconnect_request`
Request graceful disconnect of the connection. The receiving party should
stop tasks that send events and requests over the link and wait for
pending requests it has over the link to complete, but it should not
stop responding to requests sent to it. Only after this process is done
should the response be sent.
### `prepare_disconnect_response`
Signals the preparation for disconnect is complete and that the
connection can be closed. The party that initiatied the prepare
disconnect request may still send other requests/events over the link to
finish its part of the disconnection proceedure.
If the error response variant is received it should be logged if
possible and treated as a success.
+661
View File
@@ -0,0 +1,661 @@
Writing Plugins
===============
Plugins for Clusterio are classes written in JavaScript that run under
Node.js. The plugin classes have pre-defined hooks that are called
during various stages and operations of Clusterio.
Contents
--------
- [Plugin Structure](#plugin-structure)
- [Defining the plugin class](#defining-the-plugin-class)
- [Plugin Configuration](#plugin-configuration)
- [Communicating with Factorio](#communicating-with-factorio)
- [Defining Link Messages](#defining-link-messages)
- [Defining Events](#defining-events)
- [Defining Requests](#defining-requests)
- [Sending Link Messages](#sending-link-messages)
- [Handling connection events](#handling-connection-events)
- [Collecting Statistics](#collecting-statistics)
- [Adding Custom Commands to clusterioctl](#adding-custom-commands-to-clusterioctl)
Plugin Structure
----------------
The basic file structure of a plugin is the following.
plugin_name/
+- info.js
+- package.json
+- master.js
+- instance.js
+- control.js
+- test/
| +- plugin.js
+- module/
+- module.json
+- plugin.lua
Clusterio plugins are Node.js packages that can be published on npm for
ease of installation and distribution. The usual guides for creating
such packages apply. At minimum the `package.json` file must contain a
version entry.
A possible workflow for developing plugins is to place the plugin in a
sub-directory of where clusterio has been installed, and rely on Node.js
searching up the folder heirarchy for it to find `@clusterio/lib`. To
add it to `plugin-list.json` so that it gets loaded use the `plugin add
<path>` sub-command to either clusteriomaster, clusterioslave or
clusterioctl. Note that it's important that the path starts with ./ or
../ (use .\ or ..\ on Windows).
For a plugin the most important file is the `info.js` file. Without it
the plugin will not recognized by Clusterio. Here's an example of it:
const libLink = require('@clusterio/lib/link'); // For messages
module.exports = {
name: "foo_frobber",
title: "Foo Frobber",
description: "Does advanced frobnication",
instanceEntrypoint: "instance",
masterEntrypoint: "master",
messages: {
/* See below */
},
};
The following properties are recognized:
**name**:
Internal name of the plugin. Must be the same as the plugin's
directory name
**title**:
Name of the plugin as shown to users. Currently not used.
**description**:
Brief description of what the plugin does. Currently not used.
**instanceEntrypoint**:
Path to a Node.js module relative to the plugin directory which
contains the InstancePlugin class definition for this plugin. This
is an optional paramater. A plugin may have code only for instances
but it must still be loaded on the master in order for it to be
possible to load it on an instance.
**InstanceConfigGroup**:
Subclass of `PluginConfigGroup` for defining the per instance
configuration fields for this plugin. See [Plugin
Configuration](#plugin-configuration)
**masterEntrypoint**:
Path to a Node.js module relative to the plugin directory which
contains the MasterPlugin class definiton for this plugin. This is
an optional parameter. A plugin can be made that only runs on the
master server.
**MasterConfigGroup**:
Subclass of `PluginConfigGroup` for defining the master server
configuration fields for this plugin. See [Plugin
Configuration](#plugin-configuration)
**controlEntrypoint**:
Path to a Node.js module relative to the plugin directory which
contains the ControlPlugin class definition for this plugin. This
is an optional paramater. A plugin can be made that only runs on
the clusterioctl side.
**messages**:
Object with link messages definitions for this plugin. See guide
for [defining link messages](#defining-link-messages) below.
The optional module folder contains a Clusterio module that will be
patched into the save when the plugin is enabled. See the section on
[Clusterio Modules](developing-for-clusterio.md) in the Developing for
Clusterio document. The only restriction imposed on modules embedded
into plugins is that they must be named the same as the plugin.
While there is no standard for how to organize a plugin it's recommended
to put the MasterPlugin class definition into master.js and the
InstancePlugin class definition into instance.js. You can put them into
whatever file you want (even the same one for both).
For both instanceEntrypoint and masterEntrypoint the path should not end
with .js and it should use forward slashes for directory separators if
any.
Defining the plugin class
-------------------------
The plugin class should derive from its respective base class defined in
`lib/plugin`. For example, to define a MasterPlugin class the following
code can be used:
const libPlugin = require('@clusterio/lib/plugin');
class MasterPlugin extends libPlugin.BaseMasterPlugin {
async init() {
this.foo = 42;
await this.startFrobnication();
}
// ...
}
module.exports = {
MasterPlugin,
}
For the instance plugin it's exactly the same except "Master" is
replaced with "Instance", and for the clusterioctl plugin "Control" is
used. The available hooks that you can override are documented in the
base class [in lib/plugin.js](/packages/lib/plugin.js).
It's best to avoid defining a constructor, but if you insist on defining
one, forward all arguments to the base class. E.g.:
constructor(...args) {
super(...args);
// Code here
}
The arguments passed may change, and attempting to modify them will
result in unpredicatable behaviour. The async init method is always called
immediatly after the constructor, so there's little reason to do this.
Defining Configuration
----------------------
Clusterio provides a configuration system that handles storing,
distributing, editing and validating config fields for you. You can
take advantage of it by subclassing `PluginConfigGroup`, setting the
`groupName` to your plugin name, defining fields on it, finalizing it,
and passing it as either `MasterConfigGroup` or `InstanceConfigGroup` in
the `info.js` export. For example in info.js:
const libConfig = require("@clusterio/lib/config");
class MasterConfigGroup extends libConfig.PluginConfigGroup { }
MasterConfigGroup.groupName = "foo_frobber";
MasterConfigGroup.define({
name: "level",
description: "Level of frobnication done",
type: "number",
initial_value: 2,
});
MasterConfigGroup.finalize();
module.exports = {
...
MasterConfigGroup: MasterConfigGroup,
};
Code inside the `MasterPlugin` class will then be able to access the
level config field through the `Config` object at `this.master.config`,
for example in the MasterPluginClass:
async init() {
let level = this.master.config.get("foo_frobber.level");
console.log("I got a frobnication level of", level);
}
The same applies for instance configs, replace "master" with "instance"
where appropriate. See [Configuration System](configuration-system.md)
for more details on how this system works.
### Handling Invalid Configuration
If the plugin requires a certain feature to be enabled to function it
should throw an error during init if this is not the case. The most
common such feature is the save patching, which can be disabled to run
vanilla or scenarios not compatible with Clusterio. For example:
async init() {
if (!this.instance.config.get("factorio.enable_save_patching")) {
throw new Error("foo_frobber plugin requires save patching.");
}
}
Communicating with Factorio
---------------------------
For pushing data into Factorio there's RCON, which lets you send
arbitrary Lua commands to invoke whatever code you want in the game.
This is done by calling the `sendRcon` method on the instance's server object.
For example:
async onStart() {
let response = await this.instance.server.sendRcon(
"/sc rcon.print('data')"
);
// Do stuff with response.
}
Because data into Factorio is streamed at a rate of 3-6 kB/s by default,
it is recommended to avoid sending large commands as much as possible,
and to strip down the data on the ones you send to only what's strictly
necessary. You can have lua code injected into the game via the module
system and call that from RCON to avoid having to send code through the
commands.
For getting data out from Factorio there's both RCON and the `send_json`
API of the Clusterio module. Returning data via RCON is prefered if the
action is initiated from the Node.js side. The `send_json` API allows
sending JSON payloads on channels that plugins can listen to. From a
plugin you listen for an event named `ipc-channel_name` in order to get
data sent by `send_json`. For example in the plugin code:
async init() {
this.instance.server.on('ipc-my_plugin_foo', content =>
this.handleFoo(content).catch(err => console.log(
"Error handling foo:", foo
))
);
}
async handleFoo(content) {
// Do stuff with content
}
And then in the module for the plugin:
local clusterio_api = require('modules/clusterio/api')
-- inside some event handler
clusterio_api.send_json('my_plugin_foo', { data = 123 })
It's recommended to either use the plugin name as the channel name or to
prefix the channel name with the name of the plugin if you need multiple
channels. It's also important to catch any errors that might occur as
they will otherwise be propogated to the instance code and kill the
server.
Data out from Factorio does not have the same limits as data into
Factorio, RCON responses can be in 100kB range without causing issues,
and payloads to the `send_json` API can be in the 4MB range provided the
server has a fast enough storage system.
**Note:** both `send_json` and RCON can operate out of order. For
`send_json` it's possible that payloads greater than 4kB are received
after payloads that were sent at a later point in time. For RCON,
commands longer than 50 characters may end up being executed after
shorter commands sent after it.
Defining Link Messages
----------------------
You will most likely have to communicate with the master or other
instances in your plugin for it to do anything useful. For this there's
a WebSocket communication channel established between the slaves and the
master server that plugins can define their own messages to send over
it. This channel is bi-directional and all messages sent over it are
validated with a JSON schema (see [this guide][guide] for an
introduction to writing JSON schema).
[guide]: https://json-schema.org/learn/getting-started-step-by-step.html
There are currently two kinds of messages that can be defined: events
and requests. Events are simple one-way notifications that invoke a
handler on the target it's sent to. Requests are pairs of request and
response messages where the request is sent to the target and the
response is the reply back from the target. The requests are similar to
HTTP requests, in that both parties of a link may initiate one.
### Defining Events
Events are defined as properties of the messages object exported by
`info.js` that map to instances of the `Event` class from `lib/link`.
The name of the property correspond to the handler invoked on the plugin
class. The Event constructor takes an object of properties that define
the event, for example the following could be defined in `info.js`:
messages: {
startFrobnication: new libLink.Event({
type: 'foo_frobber:start_frobnication',
links: ['master-slave', 'slave-instance'],
forwardTo: 'instance',
eventProperties: {
"frobnication_type": { type: "string" },
},
}),
},
This specifies an event that can be sent from the master to a slave,
and from a slave to an instance. It also specifies that the event must
contain the property `frobnication_type`, with a string value in the
data payload. It will also be forwarded by slaves to a specific
instance.
The following properties are recognized by the Event constructor:
#### type
The message type sent over the wire. This must start with the name of
the plugin followed by colon and and be unique for the plugin. The type
of the message sent over the socket will have the suffix `_event`
appended to it.
#### links
An array of strings describing which links this event can be sent over.
Direction matters, `'master-slave'` means the event can be sent from the
master to the slave, but can't be sent back the other way, unless
`'slave-master'` is also present in the links array.
The available endpoints are `master`, `slave`, `instance`, and
`control`. Master talks with slave and control, and slave talks to
instance. The full chain must be specified as the individual links in
order for a message to travers multiple hops, (i.e., for a message to go
from master to instance it must have both `'master-slave'` and
`'slave-instance'` in the links array). See `forwardTo` and
`broadcastTo` for ways to forward an event to the next link in a chain.
#### forwardTo
Target to forward an event to. Can either be `'master'`, to indicate a
slave should forward it to the master server, or `'instance'`, to
indicate it should be forwarded to the instances specified by the
`instance_id` event property. This works by using a default handler for
the event at the links that forward it.
#### broadcastTo
Target to broadcast this message towards. A value of "instance" means
the event will be broadcast to all instances downstream of the target
it's sent to, but not back from where it came from. Currently, only
"instance" is supported. This means that sending an event to a slave
that slave except for the instance it came from. from an instance will
cause it to be broadcast to all instances of
#### eventProperties
Object with properties mapping to a JSON schema of that property that
specifies what's valid to send in the event. This is equivalent to
using the `properties` keyword in JSON schema, except that the
properties specified are implicitly required and additional properties
are not allowed. See [this guide][guide] for an introduction to writing
JSON schemas.
The forwardTo and broadcastTo can be combined such that specifying
`'master'` as the forwardTo value and `'instance'` as the broadcastTo
value will cause the event to be broadcast to all instances in the
cluster. For this to work, you will need to specify `instance-slave`,
`slave-master`, `master-slave`, and `slave-instance` as the links.
Keep in mind when forwarding events that if the target an event is being
forwarded to is not online, the event will be dropped. Use a request if
you need a confirmation that the message was received.
### Definining Requests
Requests are defined as properties of the messages object exported by
`info.js` that map to instances of the `Request` class from `lib/link`.
The name of the property corresponds to the handler invoked on the plugin
class. The Request constructor takes an object of properties that define
the event. For example, the following could be defined in `info.js`:
messages: {
reportFrobnication: new libLink.Request({
type: 'foo_frobber:report_frobnication',
links: ['master-slave', 'slave-instance'],
forwardTo: 'instance',
requestProperties: {
"verbosity": { type: "integer" },
},
responseProperties: {
"report": {
type: "array",
items: { type: "string" },
},
},
}),
},
This specifies a request that can be sent from the master to a slave,
and from a slave to an instance. The request data must contain the
property `verbosity` with an integer number as the value, as well as the
`instance_id` property (implied by `forwardTo: 'instance'`), and the
response sent must contain a `report` property mapping to an array of
strings. When received by a slave, it will also be forwarded to the
instance specified by `instance_id`.
The following properties are recognized by the Request constructor:
#### type
The message type sent over the wire. This must start with the name of
the plugin followed by colon and and be unique for the plugin. The type
of the message sent over the socket will have the suffix `_request`
appended to it for the request and `_response` appended to it for the
response.
#### links
An array of strings describing which links this request can be sent
over. Direction matters; `'master-slave'` means the request can be sent
from the master to the slave and the slave can reply to the master, but
the slave can't send a request to the master unless `'slave-master'` is
also present in the links array.
The available endpoints are `master`, `slave`, `instance`, and
`control`. Master talks with slave and control, and slave talks to
instance. The full chain must be specified as the individual links in
order for a message to travers multiple hops, (i.e., for a message to go
from master to instance it must have both `'master-slave'` and
`'slave-instance'` in the links array). See `forwardTo` for ways to
forward a request to the next link in a chain.
#### forwardTo
Target to forward the request to. Can either be `'master'` to indicate
a slave should forward it to the master server when receiving it from an
instance, or `'instance'` to indicate it should be forwarded to the
instances specified by the `instance_id` request property. This works
by using a default handler for the request by the links that forward it.
#### requestProperties
Object with properties mapping to a JSON schema of that property that
specifies what's valid to send in the request. This is equivalent to
using the `properties` keyword in JSON schema, except that the
properties specified are implicitly required and additional properties
are not allowed. See [this guide][guide] for an introduction to writing
JSON schemas
#### responseProperties
Same as the requestProperties only for the response sent back by the
target.
Sending Link Messages
---------------------
Link messages are sent by calling the `.send()` method on the
Event/Request instance with the link you want to send it over and the
data to send. For `InstancePlugin` code the link to the slave is the
`instance` itself, which is accessible through the `.instance` property
of the `InstancePlugin`. The `.info` property of the plugin class exposes
the data exported from the plugin's `info.js` module. In other words:
// In an InstancePlugin class
async frobnicate() {
this.info.messages.exampleEvent.send(this.instance, { foo: "bar" });
}
For the Request class the send method is async and returns the response
data received from the target it was sent to, or throws an error if the
request failed.
### Handling connection events
There are a few connection related events that plugins neeed to repsond
to in order to avoid data loss and connection problems. The most
important is the prepare disconnect for the link between master and
slave. This is signaled to `MasterPlugin` classes via the
`onPrepareSlaveDisconnect` hook and to `InstancePlugin` classes via the
`onPrepareMasterDisconnect` hook.
After the prepare disconnect the connection will be closed, which will
result in pending requests and events being dropped. Plugins must
respond to the prepare disconnect by stopping any processess it does
that send events or requests over the link in question. This can be
accomplished either through listening for the prepare disconnect hook,
or by checking the `connected` or `closing` property of the connector
for the master/slave connection. For example the sending of an event
from an `InstancePlugin` class can be stopped while the connection is
closing by using the following code:
if (!this.slave.connector.closing) {
this.info.messages.frobnicate.send(this.instance, { foo: "bar" });
}
If the event or request needs to be sent to the master it can be put
into a queue stored on the plugin instance and sent out when the
connection is established again. The re-establishement of the
connection is notified to plugins via the `connect` event to the
`onMasterConnectionEvent` and `onSlaveConnectionEvent` hooks.
The second connection event which is of lesser importance to respond to
is the `drop` connection event served through `onMasterConnectionEvent`
for `InstancePlugin` classes and through `onSlaveConnectionEvent` for
`MasterPlugin` classes. This is raised when the connection between the
master and slave in question is lost, most likely due to networking
issues. When in the dropped state the slave will keep trying to
reconnect to the master server in order to re-establish it, and if
successful no events or requests will be lost. However while in the
dropped state any requests and events sent gets queued up in memory
until the connection is re-established. This means that if your plugin
sends a lot of events or requests, they can end up being queued up in a
buffer for a long time and sent out all at once. To avoid this you
should be throtteling and/or stopping your requests/events after `drop`
has been raised, and continue back as normal when `connect` is raised.
Collecting Statistics
---------------------
Clusterio comes with its own Prometheus client implementation, one part
due to Not Invented Here and another part due to collectors in
prom-client being difficult to get to work nicely with collecting data
from plugins optionally loaded at runtime on different computers.
In its simplest form collecting data from plugins consists of defining
the metric and updating it somewhere in the plugin code. For example:
const { Counter } = require('@clusterio/lib/prometheus');
const fooMetric = new Counter(
'clusterio_foo_frobber_foo_metric', "Measures the level of foo",
);
// Somewhere in the master plugin code
fooMetric.inc();
This works for master plugins, and the metric will be automatically
available through the /metric HTTP endpoint. It's recommended that
plugin metrics follow `clusterio_<plugin_name>_<metric_name>` as the
naming scheme.
For metrics that are per-instance, you must define an `instance_id` label and
set it accordingly, for example:
const { Counter } = require('@clusterio/lib/prometheus');
const barMetric = new Gauge(
'clusterio_foo_frobber_bar_metric', "Bar instance level",
{ labels: ['instance_id'] }
);
// Somewhere in the instance plugin code
barMetric.labels(String(this.instance.config.get("instance.id"))).set(someValue);
Metrics are automatically registered to the default registry, and this
default registry is automatically polled by the master server on slaves.
This means that it's important that you place the definition of the
metric at module level so that it's not created more than once over the
lifetime of a slave. Since the metrics remember their values and would
continue to be exported after an instance is shutdown, there's code at
instance shutdown that removes all the values where the `instance_id`
label matches the id of the instance shut down.
For statistics you need to update on collection there's an `onMetrics`
hook on both master and instance plugins that is run before the
metrics in the default registry are collected.
Adding Custom Commands to clusterioctl
--------------------------------------
The control entrypoint for plugins allows you to extend clustectl with
your own commands. The creation of custom commands typically starts
with defining a command tree for the plugin:
const { Command, CommandTree } = require("@clusterio/lib/command");
const fooFrobberCommands = new CommandTree({
name: "foo-frobber", description: "Foo Frobber Plugin commands"
});
Then commands are added to the the plugin's command tree:
const info = require("./info");
fooFrobberCommands.add(new Command({
definition: ["frobnicate <type>", "Do frobnications", (yargs) => {
yargs.positional("level", {
describe: "type of frobnication", type: "string"
});
}],
handler: async function(args, control) {
await info.messages.frobnicate.send(control, {
instance_name: "Console",
content: args.message,
});
},
}));
For a command the `definition` is the arguments to pass to
[yargs.command](http://yargs.js.org/docs/#api-reference-commandcmd-desc-builder-handler)
(see also
[yargs.positional](http://yargs.js.org/docs/#api-reference-positionalkey-opt)
and
[yargs.options](http://yargs.js.org/docs/#api-reference-optionskey-opt)
for setting up positional and optional arguments to commands). The
`handler` is an async function that's invoked when the command is
executed and it's passed the parsed command line arguments and a
reference to the `Control` class of clusterioctl. It's possible to optain
a reference to the plugin class with `control.plugins.get(info.name)`.
Note that messages sent from clusterioctl needs to have
`"control-master"` as a part of the links array for it to be accepted by
the master server, see [Defining Link Messages](#defining-link-messages)
for how to define the messages that can be sent to the master.
To have the command tree become part of clusterioctl it needs to be added
to the rootCommand tree in `addCommands` callback of the Control plugin:
const libPlugin = require("@clusterio/lib/plugin");
class ControlPlugin extends libPlugin.BaseControlPlugin {
async addCommands(rootCommand) {
rootCommand.add(fooFrobberCommands);
}
}
module.exports = {
ControlPlugin,
}
+7
View File
@@ -0,0 +1,7 @@
{
"packages": [
"packages/*",
"plugins/*"
],
"version": "2.0.0-alpha.1"
}
-57
View File
@@ -1,57 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const NodeInstance_1 = require("./NodeInstance");
const Point_1 = require("./Point");
const Grid_1 = require("./Grid");
/**
* Manages all nodes within the cluster
*/
class ClusterManager {
constructor() {
this.Grid = new Grid_1.Grid();
}
/**
* Generates a x*y grid of 1x1 nodes
* @param width How wide to make the node grid
* @param height How high to make the node grid
* @param nodeNamePrefix The prefix used for the initial node names. The name will be in the format <prefix>_x_y.
*/
GenerateBasicGridOfNodes(width, height, nodeNamePrefix) {
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
let newNode = new NodeInstance_1.NodeInstance(nodeNamePrefix + "-" + (x + 1) + "-" + (y + 1), new Point_1.Point(x, y), 1, 1);
this.Grid.AddNodeToGrid(newNode);
}
}
}
/**
* Assigns all nodes that currently do not have a server assigned to the given server
* @param server Server to assign all unassigned nodes to
*/
AssignAllUnassignedNodesToServer(server) {
this.CheckServerArray(server);
this.Grid.GetNodes()
.filter(node => { node.Server === null; })
.forEach(node => {
server.AddNodeInstance(node);
node.Server = server;
});
}
/**
* Assigns a node to a server
* @param x x co-ordinate of the node
* @param y y co-ordinate of the node
* @param serverName server name
*/
AssignNodeToServer(x, y, server) {
this._nodeArray[x][y].Server = server;
}
CheckServerArray(server) {
let s = this._servers[server.Name];
if (s == null) {
this._servers[server.Name] = server;
}
}
}
exports.ClusterManager = ClusterManager;
//# sourceMappingURL=ClusterManager.js.map
-112
View File
@@ -1,112 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const child_process = require("child_process");
const needle = require("needle");
const Helpers_1 = require("./Helpers");
const fs = require("fs");
const util = require("util");
/**
* Contains all interactions between the cluster model, the
* Clusterio master.js/client.js infrastructure and the operating system.
*/
class ClusterioMasterProxy {
constructor(masterIP, masterPort, masterAuthToken, serviceHostType) {
this._masterIP = `${masterIP}:${masterPort}`;
this._needleOptionsWithTokenAuthHeader = {
headers: {
'x-access-token': masterAuthToken,
},
};
}
async RunRconCommand(instanceId, command, logDescription) {
console.log("Running command: " + logDescription);
let request = { instanceID: instanceId, command };
let response = await needle("post", this._masterIP + "/api/runCommand", request, this._needleOptionsWithTokenAuthHeader);
}
async GetSlaves() {
let response = await needle("get", this._masterIP + "/api/slaves", this._needleOptionsWithTokenAuthHeader);
let body = response.body;
let slaves = new Array();
Object.entries(body).forEach((keyValuePair, index, array) => {
let slave = keyValuePair[1];
slave.id = keyValuePair[0];
slaves.push(slave);
});
return slaves;
}
async CreateNodeInstancesOnLocalServer(nodes) {
await Helpers_1.asyncForEach(nodes, async (node) => {
await this.CreateNodeInstanceOnLocalServer(node);
});
await Helpers_1.asyncForEach(nodes, async (node) => {
await this.CreateTeleportZonesForANodeInstance(node);
});
await this.sleep(1000);
await Helpers_1.asyncForEach(nodes, async (node) => {
await this.CreateTeleportRestrictionsForANodeInstance(node);
await this.sleep(100);
});
}
async CreateNodeInstanceOnLocalServer(node) {
console.log(`Creating instance ${node.Name}...`);
await this.CreateNodeInstanceOnLocalServerUsingPm2(node);
console.log("Waiting for node to connect to cluster master...");
while (true) {
await this.sleep(1000);
//TODO: Use /api/getSlaveMeta/
let slaves = await this.GetSlaves();
let slave = slaves.find((slave, index, array) => slave.instanceName === node.Name);
if (slave != null) {
node.ClusterioWorldId = slave.id;
break;
}
}
}
async CreateNodeInstanceOnLocalServerUsingPm2(node) {
let proc = child_process.spawn("pm2", ['start', '--name', node.Name, 'client.js', '--', 'start', node.Name]);
proc.stdout.on('data', (data) => { console.log(`stdout: ${data}`); });
proc.stderr.on('data', data => { console.log(`stderr: ${data}`); });
proc.on('error', error => { console.log(`error: ${error}`); });
}
async CreateNodeInstanceOnLocalServerUsingScreen(node) {
// first call of client.js start for a new node creates map from HotPatch scenario then exits.
let proc = child_process.spawn("screen", ['-dmS', node.Name, '-L', '-Logfile', node.Name + '.log', 'node', 'client.js', 'start', node.Name]);
proc.stdout.on('data', (data) => { console.log(`stdout: ${data}`); });
proc.stderr.on('data', data => { console.log(`stderr: ${data}`); });
proc.on('error', error => { console.log(`error: ${error}`); });
console.log("Waiting for creation of new map...");
// create awaiter for fs.readFile
const readFile = util.promisify(fs.readFile);
while (true) {
await this.sleep(1000);
let fileData = await readFile(node.Name + '.log');
if (fileData.includes('Instance created')) {
break;
}
}
// start node
proc = child_process.spawn("screen", ['-dmS', node.Name, '-L', '-Logfile', node.Name + '.log', 'node', 'client.js', 'start', node.Name]);
proc.stdout.on('data', data => { console.log(`stdout: ${data}`); });
proc.stderr.on('data', data => { console.log(`stderr: ${data}`); });
proc.on('error', error => { console.log(`error: ${error}`); });
}
async CreateTeleportZonesForANodeInstance(nodeInstance) {
await Helpers_1.asyncForEach(nodeInstance.TeleportZones.values(), async (zone) => {
let o = zone.GenerateTeleportZoneInFactorioCoordinates();
let cmd = `CreateZone('${o[0]}',${o[1]},${o[2]},${o[3]},${o[4]},${o[5]},true)`;
await this.RunRconCommand(nodeInstance.ClusterioWorldId, `/c remote.call('trainTeleports','runCode',\"${cmd}\")`, cmd);
});
}
async CreateTeleportRestrictionsForANodeInstance(nodeInstance) {
await Helpers_1.asyncForEach(nodeInstance.TeleportZones.values(), async (zone) => {
let o = zone.GenerateZoneRestriction();
let cmd = `CreateZoneRestriction(${o[0]},'${o[1]}','${o[2]}')`;
await this.RunRconCommand(nodeInstance.ClusterioWorldId, `/c remote.call('trainTeleports','runCode',\"${cmd}\")`, cmd);
});
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
exports.ClusterioMasterProxy = ClusterioMasterProxy;
//# sourceMappingURL=ClusterioMasterProxy.js.map
-8
View File
@@ -1,8 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/** The multiplier used to convert from the grid's coordinate system to factorio's.
* 1000 means a 1x1 grid node will have a factorio map size of 1000x1000. */
exports.GridToFactorioCoordinateSystemMultiplier = 1000;
/** The width of teleport zones when they get created. */
exports.TeleportZoneWidth = 50;
//# sourceMappingURL=Config.js.map
-177
View File
@@ -1,177 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const typescript_collections_1 = require("typescript-collections");
const Point_1 = require("./Point");
const TeleportZone_1 = require("./TeleportZone");
const TeleportZoneDirection_1 = require("./TeleportZoneDirection");
class Grid {
constructor() {
this._nodes = new typescript_collections_1.Dictionary();
this._minX = 0;
this._minY = 0;
this._maxX = 0;
this._maxY = 0;
}
GetNodes() {
return this._nodes.values();
}
/**
* Adds a node to the grid
* @param topLeftCoordinate The top left x,y coordinate of the node
* @param nodeInstance
*/
AddNodeToGrid(nodeInstance) {
let topLeftCoordinate = nodeInstance.TopLeftCoordinate;
let pointsToSave = new Array();
// check all points that make up nodeInstance for existing nodes
for (let x = topLeftCoordinate.X; x < topLeftCoordinate.X + nodeInstance.Width; x++) {
for (let y = topLeftCoordinate.Y; y < topLeftCoordinate.Y + nodeInstance.Height; y++) {
let currentPoint = new Point_1.Point(x, y);
let existingNode = this._nodes.getValue(currentPoint);
if (existingNode != null) {
throw new Error("Cannot add node to grid - collision detected with node '" + existingNode.Name + "'");
}
pointsToSave.push(currentPoint);
}
}
// no existing nodes found to assign node to these points
for (let i = 0; i < pointsToSave.length; i++) {
this._nodes.setValue(pointsToSave[i], nodeInstance);
}
//TODO: Refactor
//#region Set up teleport zones
// check surrounding points for nodes
// check north points
let y = topLeftCoordinate.Y - 1;
for (let x = topLeftCoordinate.X; x < topLeftCoordinate.X + nodeInstance.Width; x++) {
let currentPoint = new Point_1.Point(x, y);
let existingNode = this._nodes.getValue(currentPoint);
// found a node - set up teleport zone
if (existingNode != null) {
// Create/Update teleport node in existing node to new node
let teleportZoneFromExistingToNew = existingNode.TeleportZones.getValue(nodeInstance.Key);
if (teleportZoneFromExistingToNew == null) {
teleportZoneFromExistingToNew = new TeleportZone_1.TeleportZone(existingNode, TeleportZoneDirection_1.TeleportZoneDirection.South, currentPoint, currentPoint, existingNode.TeleportZones.size(), nodeInstance);
existingNode.TeleportZones.setValue(nodeInstance.Key, teleportZoneFromExistingToNew);
}
else {
teleportZoneFromExistingToNew.BottomRightCoordinate = currentPoint;
}
// Create/Update teleport node in new node to existing node
let teleportZoneFromNewToExisting = nodeInstance.TeleportZones.getValue(existingNode.Key);
let pointInNewNode = new Point_1.Point(currentPoint.X, currentPoint.Y + 1);
if (teleportZoneFromNewToExisting == null) {
teleportZoneFromNewToExisting = new TeleportZone_1.TeleportZone(nodeInstance, TeleportZoneDirection_1.TeleportZoneDirection.North, pointInNewNode, pointInNewNode, nodeInstance.TeleportZones.size(), existingNode);
nodeInstance.TeleportZones.setValue(existingNode.Key, teleportZoneFromNewToExisting);
}
else {
teleportZoneFromNewToExisting.BottomRightCoordinate = currentPoint;
}
}
}
// check south points
y = topLeftCoordinate.Y + nodeInstance.Height + 1;
for (let x = topLeftCoordinate.X; x < topLeftCoordinate.X + nodeInstance.Width; x++) {
let currentPoint = new Point_1.Point(x, y);
let existingNode = this._nodes.getValue(currentPoint);
// found a node - set up teleport zone
if (existingNode != null) {
// Create/Update teleport node in existing node to new node
let teleportZoneFromExistingToNew = existingNode.TeleportZones.getValue(nodeInstance.Key);
if (teleportZoneFromExistingToNew == null) {
teleportZoneFromExistingToNew = new TeleportZone_1.TeleportZone(existingNode, TeleportZoneDirection_1.TeleportZoneDirection.North, currentPoint, currentPoint, existingNode.TeleportZones.size(), nodeInstance);
existingNode.TeleportZones.setValue(nodeInstance.Key, teleportZoneFromExistingToNew);
}
else {
teleportZoneFromExistingToNew.BottomRightCoordinate = currentPoint;
}
// Create/Update teleport node in new node to existing node
let teleportZoneFromNewToExisting = nodeInstance.TeleportZones.getValue(existingNode.Key);
let pointInNewNode = new Point_1.Point(currentPoint.X, currentPoint.Y - 1);
if (teleportZoneFromNewToExisting == null) {
teleportZoneFromNewToExisting = new TeleportZone_1.TeleportZone(nodeInstance, TeleportZoneDirection_1.TeleportZoneDirection.South, pointInNewNode, pointInNewNode, nodeInstance.TeleportZones.size(), existingNode);
nodeInstance.TeleportZones.setValue(existingNode.Key, teleportZoneFromNewToExisting);
}
else {
teleportZoneFromNewToExisting.BottomRightCoordinate = currentPoint;
}
}
}
// check west points
let x = topLeftCoordinate.X - 1;
for (let y = topLeftCoordinate.Y; y < topLeftCoordinate.Y + nodeInstance.Height; y++) {
let currentPoint = new Point_1.Point(x, y);
let existingNode = this._nodes.getValue(currentPoint);
// found a node - set up teleport zone
if (existingNode != null) {
// Create/Update teleport node in existing node to new node
let teleportZoneFromExistingToNew = existingNode.TeleportZones.getValue(nodeInstance.Key);
if (teleportZoneFromExistingToNew == null) {
teleportZoneFromExistingToNew = new TeleportZone_1.TeleportZone(existingNode, TeleportZoneDirection_1.TeleportZoneDirection.East, currentPoint, currentPoint, existingNode.TeleportZones.size(), nodeInstance);
existingNode.TeleportZones.setValue(nodeInstance.Key, teleportZoneFromExistingToNew);
}
else {
teleportZoneFromExistingToNew.BottomRightCoordinate = currentPoint;
}
// Create/Update teleport node in new node to existing node
let teleportZoneFromNewToExisting = nodeInstance.TeleportZones.getValue(existingNode.Key);
let pointInNewNode = new Point_1.Point(currentPoint.X + 1, currentPoint.Y);
if (teleportZoneFromNewToExisting == null) {
teleportZoneFromNewToExisting = new TeleportZone_1.TeleportZone(nodeInstance, TeleportZoneDirection_1.TeleportZoneDirection.West, pointInNewNode, pointInNewNode, nodeInstance.TeleportZones.size(), existingNode);
nodeInstance.TeleportZones.setValue(existingNode.Key, teleportZoneFromNewToExisting);
}
else {
teleportZoneFromNewToExisting.BottomRightCoordinate = currentPoint;
}
}
}
// check east points
x = topLeftCoordinate.X + nodeInstance.Width + 1;
for (let y = topLeftCoordinate.Y; y < topLeftCoordinate.Y + nodeInstance.Height; y++) {
let currentPoint = new Point_1.Point(x, y);
let existingNode = this._nodes.getValue(currentPoint);
// found a node - set up teleport zone
if (existingNode != null) {
// Create/Update teleport node in existing node to new node
let teleportZoneFromExistingToNew = existingNode.TeleportZones.getValue(nodeInstance.Key);
if (teleportZoneFromExistingToNew == null) {
teleportZoneFromExistingToNew = new TeleportZone_1.TeleportZone(existingNode, TeleportZoneDirection_1.TeleportZoneDirection.West, currentPoint, currentPoint, existingNode.TeleportZones.size(), nodeInstance);
existingNode.TeleportZones.setValue(nodeInstance.Key, teleportZoneFromExistingToNew);
}
else {
teleportZoneFromExistingToNew.BottomRightCoordinate = currentPoint;
}
// Create/Update teleport node in new node to existing node
let teleportZoneFromNewToExisting = nodeInstance.TeleportZones.getValue(existingNode.Key);
let pointInNewNode = new Point_1.Point(currentPoint.X - 1, currentPoint.Y);
if (teleportZoneFromNewToExisting == null) {
teleportZoneFromNewToExisting = new TeleportZone_1.TeleportZone(nodeInstance, TeleportZoneDirection_1.TeleportZoneDirection.East, pointInNewNode, pointInNewNode, nodeInstance.TeleportZones.size(), existingNode);
nodeInstance.TeleportZones.setValue(existingNode.Key, teleportZoneFromNewToExisting);
}
else {
teleportZoneFromNewToExisting.BottomRightCoordinate = currentPoint;
}
}
}
//#endregion
this._nodes.setValue(topLeftCoordinate, nodeInstance);
this._minX = Math.min(this._minX, topLeftCoordinate.X);
this._minY = Math.min(this._minY, topLeftCoordinate.Y);
this._maxX = Math.max(this._maxX, topLeftCoordinate.X + nodeInstance.Width);
this._maxY = Math.max(this._maxY, topLeftCoordinate.Y + nodeInstance.Height);
}
get Width() {
return this._maxX - this._minX;
}
get Height() {
return this._maxY - this._minY;
}
get TopLeftCoordinate() {
return new Point_1.Point(this._minX, this._minY);
}
get BottomRightCoordinate() {
return new Point_1.Point(this._maxX, this._maxY);
}
}
exports.Grid = Grid;
//# sourceMappingURL=Grid.js.map
-10
View File
@@ -1,10 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//TODO: Make generic
async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
}
exports.asyncForEach = asyncForEach;
//# sourceMappingURL=Helpers.js.map
-3
View File
@@ -1,3 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=IApiSlaves.js.map
-3
View File
@@ -1,3 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=IClusterioMasterProxy.js.map
-27
View File
@@ -1,27 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
console.log("started.");
const ClusterioMasterProxy_1 = require("./ClusterioMasterProxy");
const ClusterManager_1 = require("./ClusterManager");
const argv = require('minimist')(process.argv.slice(2));
let configPath = argv['configPath'];
if (configPath === undefined || configPath === null || configPath === "") {
throw new Error("configPath is null.");
}
;
let config = require(configPath);
let proxy = new ClusterioMasterProxy_1.ClusterioMasterProxy(config.masterIP, config.masterPort, config.masterAuthToken);
let manager = new ClusterManager_1.ClusterManager();
manager.GenerateBasicGridOfNodes(2, 2, 'GridLockTest_');
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// async function test() {
// await asyncForEach([1,2,3], async (v, i, a) => {
// console.log("test" + i);
// await sleep(5000);
// });
// }
// test();
proxy.CreateNodeInstancesOnLocalServer(manager.Grid.GetNodes());
//# sourceMappingURL=ModelController.js.map
-24
View File
@@ -1,24 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
console.log("started.");
const ClusterioMasterProxy_1 = require("./ClusterioMasterProxy");
const ClusterManager_1 = require("./ClusterManager");
const argv = require('minimist')(process.argv.slice(2));
let configPath = argv['configPath'];
if (configPath === undefined || configPath === null || configPath === "") {
throw new Error("configPath is null.");
}
;
let config = require(configPath);
let proxy = new ClusterioMasterProxy_1.ClusterioMasterProxy(config.masterIP, config.masterPort, config.masterAuthToken);
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// main
(async () => {
let manager = new ClusterManager_1.ClusterManager();
manager.GenerateBasicGridOfNodes(2, 2, 'GridLockTest');
await proxy.CreateNodeInstancesOnLocalServer(manager.Grid.GetNodes());
return;
})();
//# sourceMappingURL=ModelDemo.js.map
-61
View File
@@ -1,61 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
console.log("Started.");
const ClusterioMasterProxy_1 = require("./ClusterioMasterProxy");
const ClusterManager_1 = require("./ClusterManager");
const argv = require('minimist')(process.argv.slice(2));
// arg: configPath
// description: should point to the clusterio config.json file.
let configPath = argv['configPath'];
if (configPath === undefined || configPath === null || configPath === "") {
configPath = "../../config.json";
}
;
let config = require(configPath);
// arg: mode
// description: should be GenerateBasicGridOfNodes for now. Can extend in future.
let mode = argv['mode'];
if (mode === undefined || mode === null || mode === "") {
throw new Error("mode is null. mode must be 'GenerateBasicGridOfNodes' (for now).");
}
;
// arg: serviceHostType
// description: what program to use to host the client.js instances.
let serviceHostType = argv['serviceHostType'];
if (serviceHostType === undefined || serviceHostType === null || serviceHostType === "") {
throw new Error("serviceHostType is null. serviceHostType must be either 'pm2' or 'screen'.");
}
;
if (serviceHostType != "pm2" && serviceHostType != "screen") {
throw new Error("serviceHostType must be either 'pm2' or 'screen'.");
}
// arg: width
// description: width of node grid
let width = argv['width'];
if (width === undefined || width === null || width === "") {
throw new Error("width is null. width must be an int > 0.");
}
;
// arg: height
// description: height of node grid
let height = argv['height'];
if (height === undefined || height === null || height === "") {
throw new Error("height is null. height must be an int > 0.");
}
;
// arg: namePrefix
// description: namePrefix of node grid
let namePrefix = argv['namePrefix'];
if (namePrefix === undefined || namePrefix === null || namePrefix === "") {
throw new Error("namePrefix is null.");
}
;
let proxy = new ClusterioMasterProxy_1.ClusterioMasterProxy(config.masterIP, config.masterPort, config.masterAuthToken, serviceHostType);
// main
(async () => {
let manager = new ClusterManager_1.ClusterManager();
manager.GenerateBasicGridOfNodes(width, height, namePrefix);
await proxy.CreateNodeInstancesOnLocalServer(manager.Grid.GetNodes());
return;
})();
//# sourceMappingURL=ModelRunner.js.map
-29
View File
@@ -1,29 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const Point_1 = require("./Point");
const typescript_collections_1 = require("typescript-collections");
/**
* An instance of a Factorio server. Multiple nodes can run on the same server.
*/
class NodeInstance {
constructor(name, topLeftCoordinate, width, height) {
this.TeleportZones = new typescript_collections_1.Dictionary();
this.Name = name;
this.Width = width;
this.Height = height;
this.TopLeftCoordinate = topLeftCoordinate;
}
get BottomRightCoordinate() {
return new Point_1.Point(this.TopLeftCoordinate.X + this.Width, this.TopLeftCoordinate.Y + this.Height);
}
// public AddTeleportZone(name: string, topLeftCoordinate: Point, bottomRightCoordinate: Point) {
// this.TeleportZones.push(new TeleportZone(this, topLeftCoordinate, bottomRightCoordinate, name));
// }
AddTeleportZonePoint() {
}
get Key() {
return this.Name;
}
}
exports.NodeInstance = NodeInstance;
//# sourceMappingURL=NodeInstance.js.map
-57
View File
@@ -1,57 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
class Point {
/**
* Constructs a Point. Will not accept NaNs.
* @param x A number. Cannot be NaN.
* @param y A number. Cannot be NaN.
*/
constructor(x, y) {
this.toString = () => {
return "(" + this.X + "," + this.Y + ")";
};
if (Number.isNaN(x)) {
throw new Error("x is NaN. Will not construct Point.");
}
if (Number.isNaN(y)) {
throw new Error("y is NaN. Will not construct Point.");
}
this.X = x;
this.Y = y;
}
/**
* Constructs a Point from a string representation in the format (x,y). Will not accept NaNs.
* @param vectorAsString A string in the format (x,y). Will not accept NaNs.
*/
static FromString(vectorAsString) {
this.CheckChar('(', vectorAsString[0], vectorAsString);
this.CheckChar(')', vectorAsString[vectorAsString.length - 1], vectorAsString);
if (vectorAsString.indexOf(',') === -1) {
throw new Error("vectorAsString is an invalid format. Cannot find ','. fullString: " + vectorAsString);
}
let splitStr = vectorAsString.substr(1, vectorAsString.length - 2).split(',');
if (splitStr.length != 2) {
throw new Error("vectorAsString is an invalid format. Expecting string in format (x,y). fullString: " + vectorAsString);
}
let x = parseInt(splitStr[0]);
let y = parseInt(splitStr[1]);
let v = new Point(x, y);
return v;
}
static CheckChar(expectedChar, actualChar, fullString) {
if (actualChar != expectedChar) {
throw new Error("vectorAsString is an invalid format. Expecting: '" + expectedChar + "'. Actual: '" + actualChar + "'. fullString: " + fullString);
}
}
equals(obj) {
return this.X === obj.X && this.Y === obj.Y;
}
Add(p) {
return new Point(this.X + p.X, this.Y + p.Y);
}
Subtract(p) {
return new Point(this.X - p.X, this.Y - p.Y);
}
}
exports.Point = Point;
//# sourceMappingURL=Point.js.map
-16
View File
@@ -1,16 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/**
* A server. Can contain multiple nodes (Factorio server instances).
*/
class Server {
constructor(name) {
this.Nodes = new Array();
this.Name = name;
}
AddNodeInstance(node) {
this.Nodes.push(node);
}
}
exports.Server = Server;
//# sourceMappingURL=Server.js.map
-8
View File
@@ -1,8 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var ServiceHostType;
(function (ServiceHostType) {
ServiceHostType[ServiceHostType["pm2"] = 0] = "pm2";
ServiceHostType[ServiceHostType["screen"] = 1] = "screen";
})(ServiceHostType = exports.ServiceHostType || (exports.ServiceHostType = {}));
//# sourceMappingURL=ServiceHostType.js.map
-64
View File
@@ -1,64 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const Point_1 = require("./Point");
const TeleportZoneDirection_1 = require("./TeleportZoneDirection");
const Config = require("./Config");
class TeleportZone {
constructor(parentNode, direction, topLeftCoordinate, bottomRightCoordinate, index, targetNode) {
this.TopLeftCoordinate = topLeftCoordinate;
this.BottomRightCoordinate = bottomRightCoordinate;
this.ParentNode = parentNode;
this.Direction = direction;
this.Index = index;
this.TargetNode = targetNode;
}
get Name() {
return TeleportZoneDirection_1.TeleportZoneDirection[this.Direction] + " to " + this.TargetNode.Name;
}
// public AddRestriction(restrictingTeleportZone: TeleportZone) {
// this.Restrictions.push(new TeleportZoneRestriction(this, restrictingTeleportZone));
// }
/**
* Converts this teleport zone into Factorio map co-ordinates
* @returns [name, topleft.x, topleft.y, width, height, zoneIndex]
*/
GenerateTeleportZoneInFactorioCoordinates() {
// get where this zone is relative to the node itself
let topLeftRelativeCoordinate = this.TopLeftCoordinate.Subtract(this.ParentNode.TopLeftCoordinate);
// a grid coordinate point is equivalent to a 1x1 node, so bottomright = topleft.
// add (1,1) to translate from area representation to coordinates representing actual points.
let bottomRightRelativeCoordinate = this.BottomRightCoordinate
.Subtract(this.ParentNode.TopLeftCoordinate)
.Add(new Point_1.Point(1, 1));
// Factorio map is e.g. (-500,-500) to (500, 500) for a 1000x1000 node.
// We therefore need to offset our co-ordinates so that (0,0) is the center and not (width/2, height/2)
let m = Config.GridToFactorioCoordinateSystemMultiplier;
let factorioTeleportZoneTopLeft = new Point_1.Point(topLeftRelativeCoordinate.X * m - m / 2, topLeftRelativeCoordinate.Y * m - m / 2);
let factorioTeleportZoneBottomRight = new Point_1.Point(bottomRightRelativeCoordinate.X * m - m / 2, bottomRightRelativeCoordinate.Y * m - m / 2);
let zoneWidth = factorioTeleportZoneBottomRight.X - factorioTeleportZoneTopLeft.X;
let zoneHeight = factorioTeleportZoneBottomRight.Y - factorioTeleportZoneTopLeft.Y;
let tl = factorioTeleportZoneTopLeft;
let br = factorioTeleportZoneBottomRight;
let zw = Config.TeleportZoneWidth;
switch (this.Direction) {
case TeleportZoneDirection_1.TeleportZoneDirection.North:
return [this.Name, tl.X, tl.Y, zoneWidth, zw, this.Index + 1];
case TeleportZoneDirection_1.TeleportZoneDirection.West:
return [this.Name, tl.X, tl.Y, zw, zoneHeight, this.Index + 1];
case TeleportZoneDirection_1.TeleportZoneDirection.South:
return [this.Name, tl.X, br.Y - zw, zoneWidth, zw, this.Index + 1];
case TeleportZoneDirection_1.TeleportZoneDirection.East:
return [this.Name, br.X - zw, tl.Y, zw, zoneHeight, this.Index + 1];
}
}
/**
* Generates a zone restriction from this teleport zone to target zone
* @returns [zoneIndex, targetNodeName, targetZoneName]
*/
GenerateZoneRestriction() {
let targetZone = this.TargetNode.TeleportZones.getValue(this.ParentNode.Key);
return [this.Index + 1, this.TargetNode.Name, targetZone.Name];
}
}
exports.TeleportZone = TeleportZone;
//# sourceMappingURL=TeleportZone.js.map
-10
View File
@@ -1,10 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var TeleportZoneDirection;
(function (TeleportZoneDirection) {
TeleportZoneDirection[TeleportZoneDirection["North"] = 0] = "North";
TeleportZoneDirection[TeleportZoneDirection["East"] = 1] = "East";
TeleportZoneDirection[TeleportZoneDirection["South"] = 2] = "South";
TeleportZoneDirection[TeleportZoneDirection["West"] = 3] = "West";
})(TeleportZoneDirection = exports.TeleportZoneDirection || (exports.TeleportZoneDirection = {}));
//# sourceMappingURL=TeleportZoneDirection.js.map
-10
View File
@@ -1,10 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
class TeleportZoneRestriction {
constructor(parentTeleportZone, restrictingTeleportZone) {
this.ParentTeleportZone = parentTeleportZone;
this.RestrictingTeleportZone = restrictingTeleportZone;
}
}
exports.TeleportZoneRestriction = TeleportZoneRestriction;
//# sourceMappingURL=TeleportZoneRestriction.js.map
-13
View File
@@ -1,13 +0,0 @@
"use strict";
function __export(m) {
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
}
Object.defineProperty(exports, "__esModule", { value: true });
__export(require("./ClusterManager"));
__export(require("./NodeInstance"));
__export(require("./Server"));
__export(require("./TeleportZone"));
__export(require("./TeleportZoneRestriction"));
__export(require("./Point"));
__export(require("./Grid"));
//# sourceMappingURL=index.js.map
-28
View File
@@ -1,28 +0,0 @@
const jwt = require('jsonwebtoken');
// const config = require("./../config");
module.exports = function(config){
return {
middleware: function (req, res, next) {
var token = req.headers['x-access-token'];
if(!token) return res.status(401).send({ auth: false, message: 'No token provided.', endpoint: req.route.path});
jwt.verify(token, config.masterAuthSecret, function(err, decoded) {
if(err) return res.status(500).send({ auth: false, message: 'Failed to authenticate token.', endpoint: req.route.path});
next();
});
},
check: async function(token){
return new Promise((resolve, reject) => {
jwt.verify(token, config.masterAuthSecret, function(err, decoded) {
if(err){
resolve({ok:false, msg:err})
} else {
resolve({ok:true, msg:"Successfully authenticated"});
}
});
});
}
}
};
@@ -1,9 +0,0 @@
module.exports = async function checkHotpatchInstallation(messageInterface){
let yn = await messageInterface("/silent-command if remote.interfaces['hotpatch'] then rcon.print('true') else rcon.print('false') end");
yn = yn.replace(/(\r\n\t|\n|\r\t)/gm, "");
if(yn == "true"){
return true;
} else if(yn == "false"){
return false;
}
}
-16
View File
@@ -1,16 +0,0 @@
const fs = require("fs-extra");
const getLua = require("./getLua.js");
let commandCache;
module.exports = async function getCommand(file, COMPRESS = true){
commandCache = commandCache || {};
if(!commandCache[file]){
commandCache[file] = await getLua(file);
return commandCache[file];
} else if(typeof commandCache[file] == "string"){
return commandCache[file];
} else {
throw new Error("Command not found");
}
}
-40
View File
@@ -1,40 +0,0 @@
const needle = require("needle");
module.exports = function(config){
const instances = [];
if(config){
return function getInstanceName(instanceID){
return new Promise((resolve, reject) => {
let instance = instances[instanceID];
if(!instance){
needle.get(config.masterIP+":"+config.masterPort+ '/api/slaves', (err, response) => {
if(err || response.statusCode != 200) {
console.log("Unable to get JSON master/api/slaves, master might be unaccessible");
} else if (response && response.body) {
if(Buffer.isBuffer(response.body)) {console.log(response.body.toString("utf-8")); reject(new Error())}
try {
for (let index in response.body)
instances[index] = response.body[index].instanceName;
} catch (e){
console.log(e);
return null;
}
instance = instances[instanceID]
if (!instance) instance = instanceID; //somehow the master doesn't know the instance
resolve(instance);
}
});
} else {
resolve(instance);
}
});
}
} else {
return async function thisAintGonnaWork(){
let err = new Error("getInstanceName from lib/clusterTools needs to be called with {config} to work");
console.log(err);
throw err;
}
}
}
-40
View File
@@ -1,40 +0,0 @@
const fs = require("fs-extra");
module.exports = function getLua(filePath, COMPRESS_LUA = true){
return new Promise((resolve, reject) => {
fs.readFile(filePath, "utf8", (err, contents) => {
if(err){
reject(err);
} else {
// split content into lines
contents = contents.split(/\r?\n/);
// join those lines after making them safe again
contents = contents.reduce((acc, val) => {
val = val.replace(/\\/g ,'\\\\');
// remove leading and trailing spaces
val = val.trim();
// escape single quotes
val = val.replace(/'/g ,'\\\'');
// remove single line comments
let singleLineCommentPosition = val.indexOf("--");
let multiLineCommentPosition = val.indexOf("--[[");
if(multiLineCommentPosition === -1 && singleLineCommentPosition !== -1) {
val = val.substr(0, singleLineCommentPosition);
}
return acc + val + '\\n';
}, ""); // need the "" or it will not process the first row, potentially leaving a single line comment in that disables the whole code
// console.log(contents);
// this takes about 46 ms to minify train_stop_tracking.lua in my tests on an i3 6100u
if(COMPRESS_LUA) contents = require("luamin").minify(contents);
resolve(contents);
}
});
});
}
-41
View File
@@ -1,41 +0,0 @@
/**
Functions frequently needed from different places in the cluster. Ex:
* Loading Lua scripts
* Checking if hotpatch is installed
* Getting an instance's displayname from their ID
*/
module.exports = ({config} = {}) => ({
/**
async function to get Lua from a file, remove comments, minify and return.
arg1 is the file path
arg2 is whether or not to compress the Lua. Makes debugging harder, but reduces bandwidth when many players are online. Recommended for polling commands. Defaults to true.
Promise resolves with a string of Lua.
*/
getLua: require("./getLua.js"),
/**
async function to check whether hotpatch is installed in a server.
First argument is the servers async messageInterface for rcon return values
Promise resolves with boolean.
*/
checkHotpatchInstallation: require("./checkHotpatchInstallation.js"),
/**
async function to get an instances displayname from their instanceID
arg1 is the instanceID
Promise resolves with a string, either the displayName or instanceID if not found.
*/
getInstanceName: require("./getInstanceName.js")(config),
/**
async function same as getLua, but using an in memory cache to lower FS operations.
arg1 is the file path
arg2 is whether or not to compress, default true
Promise resolves with a string of Lua.
*/
getCommand: require("./getCommand.js"),
})
-119
View File
@@ -1,119 +0,0 @@
//Sigmoids...
function sigmoid0(x)
{
return x/(1+Math.abs(x));
}
function sigmoid1(x)
{
if (x>=0.0) {
return 1.0
} else {
return 0.0
}
}
function sigmoid2(x)
{
if (x>=0.0){
return x
} else {
return 0.0
}
}
function sigmoid3(x)
{
return x;
}
//DOSE - Doses items for item X slave Y
//Numreq - number of items that are requested for item X for slave Y
//Instore - number of items for item X in master gauge (currently)
//store_last_tick - Number of items for item X in master gauge at time of last tick
//dole - Current dole for item X
//carry - carry for item X slave Y
//prev_req - Numreq for previous request for item X slave Y
//* doleinfo_last - Object with 2 values for last "tick period": numreq - total number or requests for item, numslave - number of slaves requesting that item
//debt - debt for item X slave Y
//Returns: Array of 3 elements
//0.Number of items to give in that dose
//1.New dole for item X
//2.New carry for item X slave Y
//3.New debt
function Dose(numreq,instore,store_last_tick,dole,carry,prev_req,numreq_total_adj,debt)
{
numreq=Number(numreq);
instore=Number(instore);
var instore_adj=instore;
if (instore_adj==0) instore_adj=0.1;
var b=Math.trunc(prev_req/instore_adj*numreq);
var inp1=[numreq,instore_adj,store_last_tick/numreq,dole,carry];//0..4
var inp2=[prev_req/numreq,numreq/numreq_total_adj,store_last_tick/numreq_total_adj,prev_req/instore_adj,b,numreq_total_adj,store_last_tick,debt];//5..12
var inputs=inp1.concat(inp2);
var internal=new Array(20).fill(0);
var outputs=[0,0,0,0];
//NN: Please don't touch :), code is generated
outputs[0]=inputs[7]*0.8058582544;
outputs[0]=sigmoid2(outputs[0]);
outputs[1]=sigmoid0(outputs[1]);
outputs[2]=sigmoid0(outputs[2]);
outputs[3]=inputs[6]*0.2294657379;
outputs[3]=sigmoid0(outputs[3]);
debt=debt || 0;
if (outputs[0]>0.99) outputs[0]=1.0;
var res1=Number(outputs[0])*numreq+outputs[3]+debt;
var res_capped=Math.round(res1);
if (res_capped>numreq) res_capped=numreq;
if (res_capped>instore) res_capped=instore;
if (res_capped<0) res_capped=0;
res1=res1 || 0;
res_capped=res_capped || 0;//Safeguard against wrong values
debt=res1-res_capped;
debt_cap=Math.round(numreq*1.5)+5;
if (debt>debt_cap) debt=debt_cap;
carry=sigmoid0(outputs[2]*carry+outputs[0]) || 0;
return [res_capped,dole+outputs[1],carry,debt];
}
//TICK - Updates values for item X, should be called once for each item for each round of all slaves requesting that item
//Instore - number of items for item X in master gauge (currently)
//dole - Current dole for item X
//store_last_tick - Number of items for item X in master gauge at time of last tick (not this one)
//* doleinfo_last - Object with 2 values for last "tick period": numreq - total number or requests for item, numslave - number of slaves requesting that item
//* doleinfo_new - Object with 2 values for new "tick period": numreq - total number or requests for item, numslave - number of slaves requesting that item
//Returns: Array of 2 elements
//0.New dole for item X
//1.Guessed percentage average of items supplied in last 10 ticks
function Tick(instore,dole,store_last_tick,numreq_total_adj)
{
var instore_adj=instore;
if (instore_adj==0) instore_adj=0.1;
var inputs=[instore,dole,store_last_tick/instore_adj,store_last_tick/numreq_total_adj,store_last_tick,numreq_total_adj];
var internal=new Array(10).fill(0);
var outputs=[0,0];
//NN: Please don't touch :), code is generated
outputs[0]=inputs[3]*0.9364774823;
outputs[0]=outputs[0]*1.0*0.4590721726;
outputs[0]=sigmoid2(outputs[0]);
outputs[1]=outputs[0]*1.828422785;
outputs[1]=sigmoid3(outputs[1]);
avg=outputs[1];
if (numreq_total_adj<0.11) avg=1;//When no item's are requested, result is 0.1 ; In that case there is no deficit, so return 1 = 100% demand fulfilled
if (avg>1) avg=1;
if (avg<0) avg=0;
return [sigmoid0(outputs[0]),avg];
}
module.exports = {Dose,Tick}
-167
View File
@@ -1,167 +0,0 @@
/**
class factorioServer
This class has the responsiblity of :
* spawning a factorio server
* Handling input via stdin or RCON
*/
const fs = require("fs");
const child_process = require("child_process");
const path = require("path");
const mkdirp = require("mkdirp");
const fileOps = require("lib/fileOps");
module.exports = class factorioServer {
constructor({
factorioDirectory = "factorio",
instanceDirectory = "instances/data",
instanceName = "Default server name",
factorioPort = Math.floor(Math.random() * 65535),
serversettings = {},
onProgress = function(type, a, b, c){
if(type == "progress") console.log("Progress: ",a," of ",b," unit: ",c);
if(type == "log") console.log(a);
},
} = {}){
this.factorioDirectory = factorioDirectory;
this.instanceDirectory = instanceDirectory;
this.instanceName = instanceName;
this.factorioPort = factorioPort;
this.serversettings = Object.assign({
"name": "Clusterio instance: " + this.instanceName,
"description": "Public factorio server running clusterio",
"tags": ["clusterio"],
"max_players": "20",
"visibility": "public",
"username": "",
"token": "",
"game_password": "",
"verify_user_identity": true,
"admins": ["Danielv123"],
"allow_commands": true,
"autosave_interval": 10,
"autosave_slots": 5,
"afk_autokick_interval": 0,
"auto_pause": false,
}, serversettings);
this.onProgress = (type, a, b, c) => {
if(typeof onProgress == "function") onProgress(type, a, b, c);
};
}
async initialize(){
if(!await this.isValidInstance()){
await this.createInstance();
}
return true;
}
async createInstance(path = this.instanceDirectory){
if(await this.isValidInstance(path)){
throw new Error("Instance already exists!");
}
// create folders
let folders = [
"/script-output/",
"/saves/",
"/mods/",
"/instanceMods/",
];
mkdirp.sync(path);
folders.forEach(folder => mkdirp.sync(path + folder));
// create files
let files = [
{name: "/script-output/output.txt", content: ""},
{name: "/script-output/orders.txt", content: ""},
{name: "/script-output/txbuffer.txt", content: ""},
{name: "/config.ini", content: "[path]\r\n\ read-data=__PATH__executable__/../../data\r\n\ write-data=__PATH__executable__/../../../" + this.instanceDirectory + "\r\n\ "},
{name: "/config.json", content: JSON.stringify({
factorioPort: this.factorioPort,
clientPassword: Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 8),
}, null, 4)},
{name: "/server-settings.json", content: JSON.stringify(this.serversettings, null, 4)},
];
files.forEach(file => {
fs.writeFile(path + file.name, file.content, doneWritingHandler);
// console.log("Created: ",path+file.name);
});
let filesToWrite = files.length;
function doneWritingHandler(){
if(!--filesToWrite){
return path;
}
}
}
async deleteInstance(path = this.instanceDirectory){
if(await this.isValidInstance(path)){
await fileOps.deleteFolderRecursive(path);
return true;
} else {
return false;
}
}
isValidInstance(path = this.instanceDirectory){
return new Promise((resolve, reject) => {
fs.stat(path, (err, stat) => {
if(err){
// console.log(err)
resolve(false);
} else {
// the instance folder exists, do some more checks
fs.readdir(path, (err, files) => {
if(files && Array.isArray(files)){
let foldersThatShouldBeThere = [
"script-output",
"saves",
"mods",
"instanceMods",
];
let instanceValidity = true;
foldersThatShouldBeThere.forEach(folder => {
if(!files.includes(folder)){
instanceValidity = false;
}
});
resolve(instanceValidity);
}
});
}
});
});
}
createMap(name = "map.zip"){
return new Promise((resolve, reject) => {
this.isValidInstance().then(isValid => {
if(!isValid){
return createInstance();
}
}).then(()=>{
this.onProgress("log", "Starting map creation");
// console.log(this.factorioDirectory + '/bin/x64/factorio')
let proc = child_process.spawn(
path.join(this.factorioDirectory, 'bin', 'x64', 'factorio'), [
'-c', path.join(this.instanceDirectory,'config.ini'),
'--create', path.join(this.instanceDirectory, 'saves',name),
'--server-settings', path.join(this.instanceDirectory, 'server-settings.json'),
],{
cwd:process.cwd(),
}
);
// console.log(proc.stdout.toString("utf8"));
proc.stdout.on("data", data => this.onProgress("log", data.toString("utf8")));
proc.on("close", code => {
this.onProgress("log", "Map created! "+code);
resolve();
});
});
});
}
}
-113
View File
@@ -1,113 +0,0 @@
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const parallel = require("mocha.parallel");
const mkdirp = require("mkdirp");
const factorioServer = require("./index.js");
describe("class factorioServer/index.js", ()=>{
it("is a class (or at least a function)", ()=>{
assert.equal(typeof factorioServer, "function");
});
describe(".deleteInstance(instanceName)", ()=>{
it("deletes an instance, defaulting to the classes instanceDirectory", async () => {
let server = new factorioServer({
instanceDirectory: path.resolve(__dirname, "_testData_deleteInstance"),
instanceName: "Unit testing instance",
});
await server.initialize();
await server.deleteInstance();
assert.equal(await server.isValidInstance(), false);
return true;
});
});
describe(".isValidInstance(instanceName)", ()=>{
it("tells us whether an instance exists or not", async () => {
let server = new factorioServer({
instanceDirectory: path.join(__dirname, "_testData_isValidInstance"),
instanceName: "Unit testing instance",
});
await server.deleteInstance();
let status = await server.isValidInstance();
assert.equal(status, false);
let x = await server.initialize(); // creates our instance
let status2 = await server.isValidInstance();
assert.equal(status2, true);
await server.deleteInstance(); // clean up folder
return true;
});
it("does not get tricked by an empty folder", async () => {
let server = new factorioServer({
instanceDirectory: path.join(__dirname, "_testData_isValidInstance_2"),
instanceName: "Unit testing instance",
});
assert.equal(await server.isValidInstance(), false);
mkdirp.sync(path.join(__dirname, "_testData_isValidInstance_2")); // create empty folder simulating instance
assert.equal(await server.isValidInstance(), false);
await server.initialize(); // create a proper instance
assert.equal(await server.isValidInstance(), true);
await server.deleteInstance(); // delete the instance and ensure we see that it is in fact deleted
assert.equal(await server.isValidInstance(), false);
return true;
}).timeout(5000);
});
describe(".initialize()", ()=>{
it("creates an instance", done => {
let server = new factorioServer({
instanceDirectory: path.resolve(__dirname, "_testData_initialize"),
instanceName: "Unit testing instance",
});
server.initialize().then(()=>{
return server.deleteInstance();
}).then(() => {
done();
}).catch(e => console.log(e));
});
});
describe(".createMap(mapName.zip)", ()=>{
it("creates a map file as instanceDirectory/saves/map.zip", async function(){
this.timeout(20000); // these test often takes a lot of time in the map creation step (4500 ms on i3 dev system)
let relativePath = path.relative('.', __dirname);
let server = new factorioServer({
instanceDirectory: path.join(relativePath, "_testData_createMap"),
instanceName: "Unit testing instance",
onProgress: false,
});
await server.initialize();
let maps = fs.readdirSync(path.join(relativePath, "_testData_createMap", "saves"));
assert.equal(maps.length, 0);
await server.createMap();
let maps2 = fs.readdirSync(path.join(relativePath, "_testData_createMap", "saves"));
assert(maps2.includes("map.zip"));
await server.deleteInstance();
return true;
});
it("supports custom filenames", async function(){
this.timeout(20000);
let relativePath = path.relative('.', __dirname);
let server = new factorioServer({
instanceDirectory: path.join(relativePath, "_testData_createMap_2"),
instanceName: "Unit testing instance",
onProgress: false,
});
await server.initialize();
let maps = fs.readdirSync(path.join(relativePath, "_testData_createMap_2", "saves"));
assert.equal(maps.length, 0);
await server.createMap("mapWithCustomName.zip");
let maps2 = fs.readdirSync(path.join(relativePath, "_testData_createMap_2", "saves"));
assert(maps2.includes("mapWithCustomName.zip"));
await server.deleteInstance();
return true;
});
});
});
-120
View File
@@ -1,120 +0,0 @@
const fs = require("fs");
const path = require("path");
// gets newest file in a directory
// dir is directory
// files is array of filenames
// callback(err, filename string);
var getNewestFile = function(dir, files, callback) {
if (!callback) throw new Error("Missing callback");
if (!files || (files && files.length === 0)) {
callback(new Error("No files"));
return;
}
// we are appending the file name to the directory, without the
// "/" we would get paths like /folderfile.txt instead of /folder/file.txt
if(dir[dir.length-1] != "/"){
dir += "/";
}
var newest = { file: files[0] };
var checked = 0;
fs.stat(dir + newest.file, function(err, stats) {
if(err) {
callback(err);
} else {
newest.mtime = stats.mtime;
for (var i = 0; i < files.length; i++) {
var file = files[i];
(function(file) {
fs.stat(file, function(err, stats) {
++checked;
if (stats.mtime.getTime() > newest.mtime.getTime()) {
newest = { file : file, mtime : stats.mtime };
}
if (checked == files.length) {
callback(undefined,newest);
}
});
})(dir + file);
}
}
});
}
// get names of all directories in directory
var getDirectoriesSync = function(srcpath) {
return fs.readdirSync(srcpath).filter(function (file) {
return fs.statSync(path.join(srcpath, file)).isDirectory();
});
}
// get names of all files in a directory
var getFileNamesSync = function(srcpath) {
return fs.readdirSync(srcpath).filter(function (file) {
return !fs.statSync(path.join(srcpath, file)).isDirectory();
});
}
// does what it says on the tin
var deleteFolderRecursiveSync = function(path) {
if (fs.existsSync(path)) {
fs.readdirSync(path).forEach(function (file, index) {
var curPath = path + "/" + file;
if (fs.lstatSync(curPath).isDirectory()) { // recurse
deleteFolderRecursiveSync(curPath);
} else { // delete file
fs.unlinkSync(curPath);
}
});
fs.rmdirSync(path);
return true;
} else {
return false;
}
};
var deleteFolderRecursive = function(path){
return new Promise((resolve, reject) => {
fs.readdir(path, (err, files) => {
if(err){
reject(err);
} else {
let thingsToDelete = files.length;
files.forEach(file => {
let curPath = path + "/" + file;
fs.stat(curPath, (err, stat) => {
if(!err){
if(stat.isDirectory()){
// recurse
deleteFolderRecursive(curPath).then(() => {
whenFilesDeleted();
}).catch(e => console.log(e));
} else {
fs.unlink(curPath, () => {
// console.log("Deleted "+curPath);
whenFilesDeleted();
});
}
} else {
resolve(false);
}
});
});
whenFilesDeleted();
function whenFilesDeleted(){
if(!thingsToDelete--){// var-- instead of --var because we are calling the function once too much and need the delay
fs.rmdir(path, () => {
resolve();
// console.log("Deleted "+path);
});
}
}
}
});
});
}
module.exports = {
getNewestFile,
getDirectoriesSync,
deleteFolderRecursiveSync,
deleteFolderRecursive,
getFileNamesSync,
}
-135
View File
@@ -1,135 +0,0 @@
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const fileOps = require('./fileOps');
// create folders for testing
function setupTestingEnv(){
if (!fs.existsSync(path.resolve(__dirname, "database"))){
fs.mkdirSync(path.resolve(__dirname, "database"));
}
if (!fs.existsSync(path.resolve(__dirname, "test"))){
fs.mkdirSync(path.resolve(__dirname, "test"));
}
if (!fs.existsSync(path.resolve(__dirname, "test/folder"))){
fs.mkdirSync(path.resolve(__dirname, "test/folder"));
}
if (!fs.existsSync(path.resolve(__dirname, "test/another folder"))){
fs.mkdirSync(path.resolve(__dirname, "test/another folder"));
}
if (!fs.existsSync(path.resolve(__dirname, "test/file.txt"))){
fs.writeFileSync(path.resolve(__dirname, "test/file.txt"), "contents");
}
if (!fs.existsSync(path.resolve(__dirname, "test/another file.txt"))){
fs.writeFileSync(path.resolve(__dirname, "test/another file.txt"), "more contents");
}
// we need this xfile to test a branch in fileOps.getNewestFile()s propagation
if (!fs.existsSync(path.resolve(__dirname, "test/xfile.txt"))){
fs.writeFileSync(path.resolve(__dirname, "test/xfile.txt"), "contents");
}
}
setupTestingEnv();
setupTestingEnv(); // run twice to get branch coverage
describe("fileOps.js", function(){
describe("fileOps.getDirectoriesSync()", function(){
it("gets an array of directory names from a path", function(){
let directories = fileOps.getDirectoriesSync(path.resolve(__dirname, "test"));
// because there are no arrays for some reason
assert(typeof directories == "object");
// they are sorted alphabetically because thats the way it is
assert(directories[0] == "another folder" && directories[1] == "folder");
});
it("does not return any filenames", function(){
let directories = fileOps.getDirectoriesSync(path.resolve(__dirname, "test"));
// this is a function to return directories, not files. So yeah.
assert(!directories.includes("file.txt") && !directories.includes("another file.txt"));
});
it("throws when lacking first argument", function(){
assert.throws(function(){
let files = fileOps.getDirectoriesSync();
}, Error);
});
});
describe("fileOps.getFileNamesSync(path)", function(){
it("gets names of files in directory", function(){
let files = fileOps.getFileNamesSync(path.resolve(__dirname, "test"));
assert(files.length > 0);
assert(files.includes("file.txt") && files.includes("another file.txt"));
});
it("throws when lacking first argument", function(){
assert.throws(function(){
let files = fileOps.getFileNamesSync();
}, Error);
});
});
describe("fileOps.getNewestFile()", function(){
it("gets the newest file in a directory async", function(done){
fileOps.getNewestFile(path.resolve(__dirname, "test/"), fileOps.getFileNamesSync(path.resolve(__dirname, "test")), function(err, newest){
assert(newest && typeof newest.file == "string");
done();
});
});
it("throws if callback is missing", function(){
assert.throws(function(){
fileOps.getNewestFile("test", fileOps.getFileNamesSync(path.resolve(__dirname, "test")));
}, Error);
});
it("executes callback(err) if no files are provided", function(done){
fileOps.getNewestFile(path.resolve(__dirname, "test"), [], function(err, newest){
assert(err && typeof err == "object");
done();
});
});
it("executes callback(err) if launched without a directory", function(done){
fileOps.getNewestFile("", fileOps.getFileNamesSync(path.resolve(__dirname, "test")), function(err, newest){
assert(err);
done();
});
});
it("executes callback(err) when you do stupid shit", function(done){
fileOps.getNewestFile({thisWontWork: "but not crash either"}, {lolz:"yeah..."}, function(err, newest){
assert(err);
// ENOENT: no such file or directory, stat 'C:\**\factorioClusterio\lib\test\undefined'
done();
});
});
});
describe("fileOps.deleteFolderRecursiveSync(path)", function(){
it("deletes a folder and all files and folders in it", function(){
let x = fileOps.deleteFolderRecursiveSync(path.resolve(__dirname, "test"));
assert.throws(function(){
fs.statSync(path.resolve(__dirname, "test"));
}, Error);
assert(x);
// give it some extra time because its sometimes very slow
}).timeout(5000);
it("returns false if you give it an invalid path", function(){
let x = fileOps.deleteFolderRecursiveSync("This isn't a real path");
assert(!x);
});
});
describe("fileOps.deleteFolderRecursive(path)", function(){
it("deletes a folder and all files and folders in it, just like sync version", async function(){
setupTestingEnv();
assert(fs.statSync(path.resolve(__dirname, "test")));
let x = await fileOps.deleteFolderRecursive(path.resolve(__dirname, "test"));
assert.throws(function(){
fs.statSync(path.resolve(__dirname, "test"));
}, Error);
assert(x === undefined, "Promise version of this function does not have a return value upon success");
return true;
}).timeout(5000);
it("throws like a promise does if you give it an invalid path", async function(){
let x = await fileOps.deleteFolderRecursive("Look this path is fake!")
.catch(e => {
assert(e);
assert.equal(e.code, "ENOENT");
return true;
});
});
});
});
-2
View File
@@ -1,2 +0,0 @@
const fileOps = require("./fileOps");
module.exports = fileOps;
-49
View File
@@ -1,49 +0,0 @@
var forge = require("node-forge");
forge.options.usePureJavaScript = true;
var fs = require("fs");
var mkdirp = require("mkdirp");
var pki = forge.pki;
var rsa = forge.pki.rsa;
var startTime, endTime;
module.exports = function({
bits = 2048,
certificatePath = "database/certificate",
doLogging = false,
} = {}){
function log(t){
if(doLogging) console.log("lib/generateSSLcert.js | "+t);
}
log("Generating SSL certificate with "+bits+" bits at "+certificatePath);
startTime = Date.now();
var keypair = rsa.generateKeyPair({bits: bits, e: 0x10001});
endTime = Date.now();
log("Generated 2048bit keypair in: "+(endTime-startTime)+" ms");
startTime = Date.now();
var cert = pki.createCertificate();
cert.publicKey = keypair.publicKey;
cert.serialNumber = "01";
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date(Date.now()+(1000*60*60*24*365)); // valid for one year
// self-sign certificate
cert.sign(keypair.privateKey);
// Convert certificate to pem type and write it to a file
if(certificatePath[certificatePath.length-1] != "/") certificatePath += "/";
if(!fs.existsSync(certificatePath)) {
mkdirp.sync(certificatePath);
console.log("Created directory: "+certificatePath);
}
fs.writeFileSync(certificatePath+ "cert.key", pki.privateKeyToPem(keypair.privateKey));
fs.writeFileSync(certificatePath+ "cert.crt", pki.certificateToPem(cert));
endTime = Date.now();
log("Generated certificate in: "+(endTime-startTime)+" ms");
}
-25
View File
@@ -1,25 +0,0 @@
var assert = require("assert");
var fs = require("fs");
var fileOps = require("lib/fileOps");
var generateSSLcert = require("./generateSSLcert.js")
describe("generateSSLcert.js(options)", ()=>{
it("Creates a folder with a .crt and .key file in it", ()=>{
let certPath = "lib/certTestPlsDelete";
assert(!fs.existsSync(certPath)); // if it already exsits, the test might pass because of leftover data (which is bad)
generateSSLcert({
// bits:512, // supported, but I just use the default of 2048 for this test
certificatePath:certPath,
doLogging: false,
});
assert(fs.existsSync(certPath));
assert(fs.existsSync(certPath+"/cert.key")); // private key
assert(fs.existsSync(certPath+"/cert.crt")); // certificate file
// remove certificates and data from the test
fileOps.deleteFolderRecursiveSync(certPath);
}).timeout(10000);
});
-53
View File
@@ -1,53 +0,0 @@
/**
Module for grabbing factorio locale from a local factorio install
@module getFactorioLocale
*/
const fs = require("fs");
const ini = require("ini");
const cache = {};
/**
Gets raw factorio locale from the .ini file.
Not available outside of module through exports.
@memberof getFactorioLocale
@inner
@param {string} factorioDirectory
@param {string} languageCode
@param {function} callback
*/
function getLocale(factorioDirectory, languageCode, callback){
var localeFile = factorioDirectory + '/data/base/locale/' + languageCode + '/base.cfg';
fs.readFile(localeFile, "utf8", (err, rawLocale) => {
callback(err, rawLocale);
});
}
/**
Gets factorios locale as an object, does not respect mods.
@param {string} factorioDirectory
@param {string} languageCode
@param {function} callback
*/
module.exports.asObject = function getLocaleAsObject(factorioDirectory, languageCode, callback){
if(typeof factorioDirectory != "string" || typeof languageCode != "string" || typeof callback != "function"){
throw new Error("Error: wrong parameters provided");
}
if(!cache[languageCode]){
getLocale(factorioDirectory, languageCode, (err, rawLocale) => {
if(err){
callback(err);
} else {
let processedLocale = ini.parse(rawLocale);
cache[languageCode] = processedLocale;
callback(undefined, processedLocale);
}
});
} else {
callback(undefined, cache[languageCode]);
}
}
-74
View File
@@ -1,74 +0,0 @@
const assert = require("assert");
const getFactorioLocale = require("./getFactorioLocale");
describe("getFactorioLocale.js", () => {
describe("asObject()", () => {
it("requires 2 strings and a function as parameters", () => {
let validate = (err) => {
return err == "Error: wrong parameters provided"
}
let message = "getFactorioLocale is responding incorrectly to invalid arguments"
let callback = (err,object) => true;
assert.throws(() => getFactorioLocale.asObject(), validate, message);
assert.throws(() => getFactorioLocale.asObject("hello", "I forgot my callback"), validate, message);
assert.throws(() => getFactorioLocale.asObject(undefined, undefined, callback), validate, message);
// the proper way to do it
assert.doesNotThrow(() => getFactorioLocale.asObject("factorio_0.15.27", "en", callback), validate, message);
});
it("returns a nested object with base game locale information", (done) => {
getFactorioLocale.asObject("factorio", "en", callback);
function callback(err, object){
assert.equal(typeof object, "object");
assert.equal(object["entity-name"]["fish"], "Fish");
assert.equal(object["entity-name"]["small-lamp"], "Lamp");
Object.keys(object).forEach(key => {
// first level of the nested object is always an object
assert.equal(typeof object[key], "object");
Object.keys(object[key]).forEach(key2 => {
// second level of the nested object is always a string, nearly always truthy
if(key2 != "so-long-and-thanks-for-all-the-fish"){
assert.ok(object[key][key2]);
assert.equal(typeof object[key][key2], "string");
}
});
});
done();
}
});
it("has some duplicate keys", (done) => {
getFactorioLocale.asObject("factorio", "en", callback);
function callback(err, object){
let arrayOfKeys = [];
// collect all keys in a single array
Object.keys(object).forEach(key => {
arrayOfKeys.push(key);
Object.keys(object[key]).forEach(key2 => {
arrayOfKeys.push(key2);
});
});
// check for duplicates
let duplicateKeys = [];
let i = 0;
arrayOfKeys.forEach(key => {
let o = 0;
arrayOfKeys.forEach(key2 => {
if(i != o && key == key2){
duplicateKeys.push(key);
}
o++
});
i++;
});
// we got 704 but this is a very unexact number
assert(duplicateKeys.length > 10)
done();
}
});
});
});
-17
View File
@@ -1,17 +0,0 @@
const fileOps = require("lib/fileOps");
// const config = require("./../config");
module.exports = function(config) {
return {
sync: function(){
let instances;
try {
instances = fileOps.getDirectoriesSync(config.instanceDirectory);
} catch (e) {
// there are no instances created yet, probably missing the folder
instances = [];
}
return instances;
},
}
}
-22
View File
@@ -1,22 +0,0 @@
const crypto = require('crypto');
const fs = require('fs');
/**
* Returns a promise that resolves to the SHA1 hash of the file given by path
*/
exports.hashFile = function(path) {
return new Promise(function(resolve, reject) {
let hasher = crypto.createHash('sha1')
hasher.setEncoding('hex')
hasher.on('finish', function() {
resolve(hasher.read());
});
let stream = fs.createReadStream(path);
stream.on('error', function(error) {
// the docs doesn't say anything about what's passed on error
reject(error);
});
stream.pipe(hasher);
});
}
-1
View File
@@ -1 +0,0 @@
module.exports = require("./isFactorioCommand.js");
@@ -1,23 +0,0 @@
const luamin = require("luamin");
function isFactorioCommand(command){
if(typeof command != "string"){
return false;
} else if(!command.includes("/silent-command ") && !command.includes("/c ")){
return false;
} else if(command[0] != "/"){
return false;
} else {
// check that what follows /c is valid LUA
let x = command.replace("/silent-command ", "").replace("/c ","");
let y
try{
y = luamin.minify(x);
} catch (e) {
return false;
}
// by this point it is surely a LUA command
return true;
}
}
module.exports = isFactorioCommand;
@@ -1,18 +0,0 @@
const isFactorioCommand = require("./isFactorioCommand");
const assert = require("assert");
describe("inventoryImports/isFactorioCommand.js()", function(){
it("Returns false if input is not a factorio command", function(){
assert(isFactorioCommand({}) == false, "This is not even a string so should return false");
assert(isFactorioCommand("Howdy boys!") == false, "This is not a command so should return false");
assert(isFactorioCommand("/c this ain't a command because its invalid LUA") == false, "This is not valid LUA so it should return false");
assert(isFactorioCommand("Trying to trick the system /c game.print('Hello world!')") == false, "this is still not a command");
});
it("Returns true if it is a valid command", function(){
assert(isFactorioCommand("/c game.print('hello world')"), "This is a valid command, should output true not false")
});
it("Fails to understand that leading spaces are valid", function(){
assert(isFactorioCommand(" /c game.print('Hello world!')") == false, "This should be true but often returns false unintentionally. If you see this you probably fixed the bug so change this test, OK?")
});
});
-69
View File
@@ -1,69 +0,0 @@
/**
Provides tools for managing both masters and slaves config files.
@module configManager
*/
// require modules
const asTable = require("as-table").configure({delimiter: ' | '});
const fs = require("fs");
// internal requires
var config = require("./../../config");
const objectOps = require("./../objectOps.js");
/**
console.log()s each entry in the config
@param {string} instance Either undefined, "shared" or the instance to show the config for.
*/
module.exports.displayConfig = function(instance){
console.log(asTable(this.getConfig(instance)));
}
module.exports.getConfig = async function(instance){
if(!instance || instance == "shared"){
let configArray = [];
Object.keys(config).forEach(key=>{
if(key != "__comment"){
if(typeof config[key] == "object"){
configArray.push({entry: key, value: JSON.stringify(config[key])});
} else configArray.push({entry: key, value: config[key]});
}
});
return configArray;
}
}
module.exports.editConfig = function(instance, entry, newValue){
if(!instance || instance == "shared"){
if(config[entry] && newValue){
if(objectOps.isJSON(newValue)){
config[entry] = JSON.parse(newValue);
console.log("Changed config entry "+entry+" from "+JSON.stringify(config[entry])+" to "+JSON.stringify(newValue));
// console.log("new config file:");
// console.log(JSON.stringify(config, null, " "))
fs.writeFileSync("config.json", JSON.stringify(config, null, 4));
return {
ok:true,
msg:`Changed config entry ${entry} from Object to ${JSON.stringify(newValue)}`,
}
} else {
config[entry] = newValue;
console.log("Changed config entry "+entry+" from "+config[entry]+" to '"+newValue+"'");
// console.log("new config file:");
// console.log(JSON.stringify(config, null, " "))
fs.writeFileSync("config.json", JSON.stringify(config, null, 4));
return {
ok:true,
msg:`Changed config entry ${entry} from ***** to '${newValue}'`,
};
}
} else {
return {
ok:false,
msg:`No such config parameter`,
}
}
} else {
return {
ok:false,
msg:`Editing config for single instances is not yet supported`,
}
}
};
-2
View File
@@ -1,2 +0,0 @@
const fs = require("fs-extra");
const needle = require("needle");
-277
View File
@@ -1,277 +0,0 @@
/**
Provides tools to manage mods in a clusterio install
@module modManager
*/
const asTable = require("as-table").configure({delimiter: ' | '});
const moment = require("moment");
const syncRequest = require("sync-request");
const fs = require("fs-extra");
const https = require('follow-redirects').https;
const factorioAPI = require("node-factorio-api");
const sLog = require("single-line-log").stdout;
const query = require('cli-interact').getYesNo;
const fileOps = require("lib/fileOps");
// const config = require("./../../config");
function ellipse(str, max){
return str.length > (max - 3) ? str.substring(0,max-3) + '...' : str;
}
// https://stackoverflow.com/questions/15900485/correct-way-to-convert-size-in-bytes-to-kb-mb-gb-in-javascript
function formatBytes(a,b){if(0==a)return"0 Bytes";var c=1024,d=b||2,e=["Bytes","KB","MB","GB","TB","PB","EB","ZB","YB"],f=Math.floor(Math.log(a)/Math.log(c));return parseFloat((a/Math.pow(c,f)).toFixed(d))+" "+e[f]};
module.exports = function(config){
if(!config) config = require("./../../config");
const instances = require("./../getInstances.js")(config).sync();
return {
/**
Lists mods for an instance as a table. Warning, handles printing internally.
@param {string} instance - Name of the instance or "shared"
@example
var modManager = require("modManager")(config);
modManager.listMods("testInstance");
// console.logs table of mods installed on instance
modManager.listMods("shared");
// console.logs mods located in /sharedMods as a nice table
*/
listMods: function(instance){
return new Promise((resolve, reject) => {
if(instance && instances.includes(instance)){
factorioAPI.init(false, config.instanceDirectory+'/'+instance+'/instanceMods', config.instanceDirectory+'/'+instance+'/saves', config.factorio_version);
factorioAPI.authenticate({
username: config.username,
token: config.token,
require_ownership: false
}).then(token => {
console.log("* Currently installed:");
factorioAPI.readModZips().then(list => {
console.log(asTable(list.map(mod => {
return {
name:(mod.title || mod.name),
version:mod.version,
date:mod.date,
factorio_ver:mod.factorio_version,
author:mod.author,
}
})));
});
// Had issues earlier due to bug in node-factorio-api, see https://github.com/Danacus/node-factorio-api/issues/2
// bug is resolved, thanks to @Danacus and @brotherpot
factorioAPI.getModsFromSaves().then(list => {
console.log("* Mods used in save:");
console.log(asTable(list[0].mods))
});
}).catch(reason => {
console.log("Failed to authenticate to factorio.com! Please check your connection and the username+token used for authentication in config.json");
resolve();
});
} else {
// list shared mods in /sharedMods/
factorioAPI.init(false, 'sharedMods/', '', config.factorio_version);
factorioAPI.authenticate({
username: config.username,
token: config.token,
require_ownership: false
}).then(() => {
console.log("\r\n")
console.log("* Mods shared by all instances:")
factorioAPI.getInstalledMods().then(mods => {
console.log(asTable(mods));
resolve();
});
}).catch(e => {
reject(new Error(e));
});
}
// get JSON data about GH releases
/*let res = syncRequest('GET', 'https://api.github.com/repos/Danielv123/factorioClusterioMod/releases', {"headers":{"User-Agent":"Fuck you for requiring user agents!"}});
let url = JSON.parse(res.getBody())[0].assets[0].browser_download_url;
let name = JSON.parse(res.getBody())[0].assets[0].name;
console.log(JSON.parse(res.getBody()))
*/
});
},
/**
Searches mods.factorio.com for mods matching a search term and prints
the results to console as a neat looking table
@param {string} searchTerm What to search for
*/
findMods: function(searchTerm){
return new Promise((resolve, reject) => {
factorioAPI.init(false);
factorioAPI.authenticate({
username: config.username,
token: config.token,
require_ownership: false
}).then(token => {
factorioAPI.searchMods(searchTerm).then(mods => {
let result = asTable(mods.reverse().map(mod => {
return {
title:ellipse(mod.title, 50),
name:ellipse(mod.name, 30),
downloads:mod.downloads_count,
updated: moment(mod.updated_at).format("DD.MM.YY"),
latest_version: mod.latest_release.version,
owner:mod.owner,
//license: mod.license_name,
}
}));
resolve(result);
// console.log(result);
});
}).catch(e => {
if(e.contains("Insufficient information")) console.log("We are missing some parameters required for logging in! Make sure your factorio username and token are specified in config.json.");
reject(new Error(e));
});
});
},
addMod: function(term, instance){
return new Promise((resolve, reject) => {
if(instance == "shared"){ // modDirectory has to end in /
var modDirectory = "sharedMods/";
} else {
var modDirectory = config.instanceDirectory+"/"+instance+"/instanceMods/";
}
if(term && term.length > 0){
if(term.includes("mods.factorio.com/mods/")){ // https://mods.factorio.com/mods/Danielv123/ChooChoo
// treat it as a link download
factorioAPI.init(false, modDirectory, "", config.factorio_version);
factorioAPI.authenticate({
username: config.username,
token: config.token,
require_ownership: false
}).then(() => {
factorioAPI.searchMods(term.split("/")[term.split("/").length-1]).then(body => {
console.log("Im gonna download: "+body[0].name + " in version: " + body[0].latest_release.version);
factorioAPI.downloadMod( {name: body[0].name, version: body[0].latest_release.version}).then(()=>{
console.log("Mod downloaded");
resolve();
});
});
});
} else if(term.includes("github.com")){ // https://github.com/Danielv123/factorioClusterioMod
// treat it as a github releases download
let apiURL = "https://api.github.com/repos/" + term.split("/")[3] + "/" + term.split("/")[4] + "/releases"; // https://api.github.com/repos/Danielv123/factorioClusterioMod/releases
let res = syncRequest('GET', apiURL, {"headers":{"User-Agent":"Fuck you for requiring user agents!"}});
let url = JSON.parse(res.getBody())[0].assets[0].browser_download_url;
let name = JSON.parse(res.getBody())[0].assets[0].name;
if(url) {
console.log("Downloading " + url);
let file = fs.createWriteStream(modDirectory + name);
let bytesDownloaded = 0;
let downloadStartTime = Date.now();
https.get(url, function(response) {
let bytesToDownload = response.headers['content-length'];
response.pipe(file);
response.on("data", chunk => {
bytesDownloaded += chunk.length;
let downloadTimeElapsed = Date.now() - downloadStartTime;
let bytesPerSecond = bytesDownloaded / (downloadTimeElapsed/1000);
let timeToComplete = Math.floor((bytesToDownload - bytesDownloaded)/bytesPerSecond);
sLog("Downloading "+name+" "+(bytesDownloaded/bytesToDownload*100).toPrecision(3)+"% ("+formatBytes(bytesDownloaded)+" / "+formatBytes(bytesToDownload)+")\r\n"+
"about "+timeToComplete+" seconds remaining ("+formatBytes(bytesPerSecond)+"/s)");
}).on("end", x => {
sLog.clear();
console.log(" Downloaded in "+moment.utc(moment.duration(Date.now()-downloadStartTime).as('milliseconds')).format('mm:ss')); // because sLog.clear() doesn't seem to do the job of ending the line properly
resolve();
});
});
}
} else {
// treat it as a search and download the top result
factorioAPI.init(false, modDirectory, "", config.factorio_version);
factorioAPI.authenticate({
username: config.username,
token: config.token,
require_ownership: false
}).then(() => {
factorioAPI.searchMods(term).then(body => {
console.log(body)
console.log("Im gonna download: "+body[0].name + " in version: " + body[0].latest_release.version);
factorioAPI.downloadMod( {name: body[0].name, version: body[0].latest_release.version}).then(()=>{
console.log("Mod downloaded");
resolve();
});
});
});
}
} else {
reject(new Error("Invalid search term"));
}
});
},
removeMod: function removeMod(term, instance, force = false){
return new Promise((resolve, reject) => {
if(instance == "shared"){ // modDirectory has to end in /
var modDirectory = "sharedMods/";
} else {
var modDirectory = config.instanceDirectory+"/"+instance+"/instanceMods/";
}
if(term && typeof term == "string" && term.length > 0){
let mods = fs.readdirSync(modDirectory);
let modsMatchingFilter = mods.filter(mod => mod.toLowerCase().includes(term.toLowerCase()));
if(modsMatchingFilter.length < 1){
console.log("No mods found matching search");
} else if(modsMatchingFilter.length == 1){
console.log("Deleting "+modsMatchingFilter[0]);
fs.unlinkSync(modDirectory+modsMatchingFilter[0]);
console.log("Deleted.");
} else if(modsMatchingFilter.length > 1){
console.log("Found multiple mods");
modsMatchingFilter.forEach(mod => console.log(mod));
if(query("Delete all mods matching filter?")){
let modsToDelete = modsMatchingFilter.length;
for(let i = 0; i < modsToDelete; i++){
fs.unlinkSync(modDirectory+modsMatchingFilter[i]);
console.log("Deleted "+modsMatchingFilter[i]);
}
} else console.log("Did not delete mods.")
}
resolve();
} else {
reject(new Error("Invalid search term"));
}
});
},
// 'https://api.github.com/repos/Danielv123/factorioClusterioMod/releases'
/**
Download mod from github repo by looking for the first binary download in
the latest release, saving it to /sharedMods
@param {string} url a github repo URL, for example https://api.github.com/repos/Danielv123/factorioClusterioMod/releases. Must link to /releases
*/
downloadFromGitURL: function(url){
console.log("Downloading mod from "+url);
// get JSON data about releases
let res = syncRequest('GET', url, {"headers":{"User-Agent":"Please don't require setting a user agent"}});
let downloadUrl = JSON.parse(res.getBody())[0].assets[0].browser_download_url;
let name = JSON.parse(res.getBody())[0].assets[0].name;
console.log(JSON.parse(res.getBody()))
if(downloadUrl) {
console.log(downloadUrl);
let file = fs.createWriteStream("sharedMods/"+name);
https.get(downloadUrl, function(response) {
response.pipe(file);
console.log("Downloaded "+name);
});
}
},
}
}
/*
module.exports.findUpdates = function(){
console.log("Downloading mods...");
// get JSON data about releases
let res = syncRequest('GET', 'https://api.github.com/repos/Danielv123/factorioClusterioMod/releases', {"headers":{"User-Agent":"Fuck you for requiring user agents!"}});
let url = JSON.parse(res.getBody())[0].assets[0].browser_download_url;
let name = JSON.parse(res.getBody())[0].assets[0].name;
console.log(JSON.parse(res.getBody()))
}*/
-116
View File
@@ -1,116 +0,0 @@
const fs = require("fs-extra");
const path = require("path");
const git = require("simple-git")("./sharedPlugins");
module.exports = function(config){
if(!config) config = require("./../../config");
return {
listPlugins: async function(){
let plugins = await this.getPlugins();
plugins.forEach(plug => {
console.log(`${plug.name}`.padEnd(22).substr(0,22)+` - ${plug.description}`.substr(0,80-22));
});
},
getPlugins: async function(){
let directories = await fs.readdir("./sharedPlugins");
let returnArray = [];
for(let i in directories){
let dir = directories[i];
let pluginContents = await fs.readdir(path.join("./sharedPlugins", dir));
// console.log(pluginContents)
if(pluginContents.includes("config.js") || pluginContents.includes("config.json")){
try{
var pluginConfig = require(path.join("./../../sharedPlugins", dir, "config"));
pluginConfig.pluginPath = path.join("./sharedPlugins", dir);
}catch(e){
console.log(e)
}
// check if plugin is enabled
try{
await fs.stat(path.join(pluginConfig.pluginPath, "DISABLED"))
pluginConfig.enabled = false;
}catch(e){
pluginConfig.enabled = true;
}
returnArray.push(pluginConfig);
}
}
return returnArray;
},
addPlugin: function(path){
return new Promise((resolve, reject) => {
if(path.includes("git")){ // we are decently sure this is a git repo
git.clone(path, data => resolve(data))
}
});
},
removePlugin: async function(name){
let plugins = await this.getPlugins();
for(let i in plugins){
let plug = plugins[i];
if(plug.name.includes(name)){
// console.log(`Removing plugin ${plug.name}`);
await fs.remove(plug.pluginPath);
return {
ok:true,
msg:`Deleted plugin ${plug.name}`
}
}
}
return {
ok:false,
msg:`Plugin matching "${name}" not found`,
}
},
enablePlugin: async function(name, instance){
let plugins = await this.getPlugins();
let plugin = plugins[findInArray("name", name, plugins)[0]];
if(plugin){
let files = await fs.readdir(plugin.pluginPath);
if(files.includes("DISABLED")){
await fs.unlink(path.join(plugin.pluginPath, "DISABLED"));
return {
ok:true,
msg:`Globally enabled plugin ${name}`,
}
} else {
return {
ok:true,
msg:`Plugin ${name} already enabled`,
}
}
} else {
return {
ok:false,
msg:`Found no excact match for plugin ${name}`,
}
}
},
disablePlugin: async function(name, instance){
let plugins = await this.getPlugins();
let plugin = plugins[findInArray("name", name, plugins)[0]];
if(plugin){
await fs.writeFile(path.join(plugin.pluginPath, "DISABLED"), "");
return {
ok:true,
msg:`Plugin ${name} is now disabled on this host`,
}
} else {
return {
ok:false,
msg:`Found no excact match for plugin ${name}`,
}
}
}
}
}
function findInArray(key, value, array){
let indexes = [];
for(let i in array){
if(array[i][key] && array[i][key] === value) indexes.push(i);
}
return indexes;
}
-135
View File
@@ -1,135 +0,0 @@
var mkdirp = require("mkdirp");
var http = require('http');
var https = require('https');
var fs = require('fs');
var modManager = require("./manager/modManager.js");
// modManager.addMod("clusterio", "shared");
function makeDirectory(path){
return new Promise((resolve, reject) => {
mkdirp(path, function(err){
if(err){
console.error("Unable to mkdirp "+path);
console.error(err);
resolve(false);
} else {
console.log("mkdirp "+path);
resolve(true);
}
});
});
}
function download(url, file, callback){
return new Promise((resolve, reject) => {
let pathArray = file.split("/");
pathArray.pop(); // remove filename from the end
if(pathArray.length >= 1){
// this might be an invalid path!
fs.stat(pathArray.join("/"), (err, stat) => {
if(err){
makeDirectory(pathArray.join("/")).then(()=>{
doDownload(url, file, callback);
});
} else {
doDownload(url, file, callback);
}
});
} else {
doDownload(url, file, callback);
}
function doDownload(url, file, callback){
var writeStream = fs.createWriteStream(file);
let protocol = http;
if(url.split(":")[0] == "https") protocol = https;
var request = protocol.get(url, function(response) {
response.pipe(writeStream);
response.on("end", () => {
if(callback && typeof callback == "function") callback(file);
console.log("Downloaded "+file+" from "+url);
resolve();
});
});
}
});
}
var directories = [
"sharedMods",
"instances",
];
let downloadList = [
// {
// url: "http://fbpviewer.trakos.pl/images/spritesheet.json",
// file: "static/pictures/spritesheet.js",
// callback: function(file){
// fs.writeFileSync("static/pictures/spritesheet.js", "export default\n"+fs.readFileSync("static/pictures/spritesheet.js"));
// },
// },
// {
// url: "http://fbpviewer.trakos.pl/images/spritesheet.png",
// file: "static/pictures/spritesheet.png",
// },
{
url: "https://code.jquery.com/jquery.js",
file: "static/external/jquery.js",
},
{
url: "https://code.jquery.com/ui/1.12.1/jquery-ui.js",
file: "static/external/jquery-ui.js",
},
{
url: "https://cdnjs.cloudflare.com/ajax/libs/canvasjs/1.7.0/canvasjs.js",
file: "static/external/canvasjs.js",
},
{
url: "https://momentjs.com/downloads/moment.min.js",
file: "static/external/moment.min.js",
},
{
url: "https://craig.global.ssl.fastly.net/js/mousetrap/mousetrap.min.js",
file: "static/external/mousetrap.min.js",
},
{
url: "https://fonts.googleapis.com/css?family=Roboto",
file: "static/external/css/Roboto.css",
},
{
url: "https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.4/socket.io.js",
file: "static/external/socket.io.js",
},
]
// initiate folder creations and downloads
processMkdirRecursive(processDownloadRecursive);
function processMkdirRecursive(callback){
let item = directories.shift();
makeDirectory(item).then(()=>{
if(directories.length == 0){
if(callback) callback();
} else {
processMkdirRecursive(callback);
}
});
}
function processDownloadRecursive(callback){
let item = downloadList.shift();
download(item.url, item.file, item.callback).then(()=>{
if(downloadList.length == 0){
if(callback) callback();
} else {
processDownloadRecursive(callback);
}
});
}
// generate SSL certificates if they are missing
if(!fs.existsSync("database/certificates") || !fs.existsSync("database/certificates/cert.crt") || !fs.existsSync("database/certificates/cert.key")){
var generateSSLcert = require("./generateSSLcert.js");
generateSSLcert({
certificatePath: "database/certificates",
doLogging: true,
});
}
-44
View File
@@ -1,44 +0,0 @@
/**
Helper functions related to objects
@module objectOps
*/
/**
Deep clones an object, is actually just JSON.parse(JSON.stringify()) but looks cleaner.
@param {object} obj to clone
@returns {object}
@example
a = {"thingy":"with a value"};
b = a;
a == b; // true
c = objectOps.deepclone(a);
a == c; // false
*/
module.exports.deepclone = function(obj){
return JSON.parse(JSON.stringify(obj));
}
/**
Checks if a string is valid JSON using try/catch. If passed an object it will always return true.
@param {string} string to verify
@returns {boolean}
@example
objectOps.isJSON("99 bottles of beer"); // false
objectOps.isJSON("99"); // true
objectOps.isJSON({"thingy":"that is an object"}); // true
objectOps.isJSON(JSON.stringify({"thingy":"that is an object"})); // true
*/
module.exports.isJSON = function(string){
let stringIsJson = false;
let x
try {
x = JSON.parse(string);
} catch (e){
return false;
}
if(typeof x == "object" || typeof string == "object"){
stringIsJson = true;
}
return stringIsJson;
}
-40
View File
@@ -1,40 +0,0 @@
var assert = require('assert');
var objectOps = require('./objectOps');
describe("objectOps.js", function(){
/*describe("objectOps.clone()", function(){
it("clones javascript objects", function(){
var object1 = {hello:"world", cat:{legs:4, name:"Kitty", colors:["brown", "yellow", "purple"]}};
var object2 = {};
object2 = objectOps.clone(object1);
assert.equal(object2.cat.name, object1.cat.name)
assert.equal(object1.hello, object2.hello)
});
it("does not deep clone objects", function(){
var obj1 = {hello:"world", cat:{legs:4, name:"Kitty", colors:["brown", "yellow", "purple"]}};
var obj2 = {};
obj2 = objectOps.clone(obj1);
obj1.cat.colors = "black";
assert.equal(obj1.cat.colors, "black");
assert.notEqual(obj2.cat.colors[1], "yellow");
});
});*/
describe("objectOps.deepclone()", function(){
it("deep clones javascript objects", function(){
var obj1 = {hello:"world", cat:{legs:4, name:"Kitty", colors:["brown", "yellow", "purple"]}};
var obj2 = {};
obj2 = objectOps.deepclone(obj1);
obj1.cat.colors = "black";
assert.equal(obj1.cat.colors, "black");
assert.equal(obj2.cat.colors[1], "yellow");
});
it("throws on non JSON parameters", function(){
assert.throws(function(){
let y = objectOps.deepclone(objectOps.deepclone);
});
});
});
});
-5
View File
@@ -1,5 +0,0 @@
{
"name": "lib",
"description": "Internal library for factorioClusterio"
}
-154
View File
@@ -1,154 +0,0 @@
--[[
Copyright 2018 Chrisgbk
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
-- MIT License, https://opensource.org/licenses/MIT
-- Hotpatchable control.lua for scenarios
-- Supports multiple simultaneously loaded softmods
-- This is a WIP
-- Version 1.1 alpha
-- probably some performance improvements to be made
_ENV.debug_settings = {
level = 'info',
log_to_console_only = false,
log_to_RCON = false,
log_on_tick = false,
}
local hotpatch_tools = require 'hotpatch.mod-tools'
require 'hotpatch.remote-interface'
require 'hotpatch.commands'
require 'hotpatch.gui'
-- mod code goes here
-- This is the Factorio Freeplay scenario
-- This code, and the freeplay locale files in the zip file, is the property of Wube
-- As this is part of the base mod, the developers have given permission for modders to use/adapt the assets therein, for modding Factorio
-- Any other use would require licensing/permission from Wube
-- If you don't want to run this, either comment this out before creating your scenario, OR uninstall freeplay at runtime
hotpatch_tools.static_mod('freeplay', '0.17.50', [===[
local util = require("util")
local silo_script = require("silo-script")
local created_items = function()
return
{
["iron-plate"] = 8,
["wood"] = 1,
["pistol"] = 1,
["firearm-magazine"] = 10,
["burner-mining-drill"] = 1,
["stone-furnace"] = 1
}
end
local respawn_items = function()
return
{
["pistol"] = 1,
["firearm-magazine"] = 10
}
end
for k,v in pairs(silo_script.get_events()) do
script.on_event(k, v)
end
script.on_event(defines.events.on_player_created, function(event)
local player = game.players[event.player_index]
util.insert_safe(player, global.created_items)
local r = global.chart_distance or 200
player.force.chart(player.surface, {{player.position.x - r, player.position.y - r}, {player.position.x + r, player.position.y + r}})
if not global.skip_intro then
if game.is_multiplayer() then
player.print({"msg-intro"})
else
game.show_message_dialog{text = {"msg-intro"}}
end
end
silo_script.on_event(event)
end)
script.on_event(defines.events.on_player_respawned, function(event)
local player = game.players[event.player_index]
util.insert_safe(player, global.respawn_items)
silo_script.on_event(event)
end)
script.on_configuration_changed(function(event)
global.created_items = global.created_items or created_items()
global.respawn_items = global.respawn_items or respawn_items()
silo_script.on_configuration_changed(event)
end)
script.on_load(function()
silo_script.on_load()
end)
script.on_init(function()
global.created_items = created_items()
global.respawn_items = respawn_items()
silo_script.on_init()
end)
silo_script.add_remote_interface()
silo_script.add_commands()
remote.add_interface("freeplay",
{
get_created_items = function()
return global.created_items
end,
set_created_items = function(map)
global.created_items = map
end,
get_respawn_items = function()
return global.respawn_items
end,
set_respawn_items = function(map)
global.respawn_items = map
end,
set_skip_intro = function(bool)
global.skip_intro = bool
end,
set_chart_distance = function(value)
global.chart_distance = tonumber(value)
end
})
]===])
-- Multi-file test
--[[
local files = {}
files['testfolder.anothertest.test'] = [===[
script.on_event(defines.events.on_player_changed_position, function(e) game.print('position changed') end)
]===]
files['testfolder.test'] = [===[
require 'anothertest.test'
]===]
files['test'] = [===[
require 'testfolder.test'
]===]
hotpatch_tools.static_mod('require-test', '1.0.0', [===[
require 'test'
]===], files)
--]]
-- end of mod code
@@ -1,135 +0,0 @@
local hotpatch_tools = require 'hotpatch.mod-tools'
hotpatch_tools.static_mod('hotpatch-commands', '1.0.2', [===[
--[[
Copyright 2018 Chrisgbk
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
-- MIT License, https://opensource.org/licenses/MIT
local hotpatch_tools = require 'hotpatch.mod-tools'
--load private API
-- mod installation/uninstallation support functions
-- These take a mod NAME as a first argument
local install_mod = hotpatch_tools.install_mod
local find_installed_mod = hotpatch_tools.find_installed_mod
local install_mod_file = hotpatch_tools.install_mod_file
local uninstall_mod = hotpatch_tools.uninstall_mod
-- mod interaction functions
-- These take a LOADED INDEX as a first argument, except load_mod, which takes an INSTALLED INDEX
local load_mod = hotpatch_tools.load_mod
local find_loaded_mod = hotpatch_tools.find_loaded_mod
local run_mod = hotpatch_tools.run_mod
local reset_mod = hotpatch_tools.reset_mod
local reset_mod_events = hotpatch_tools.reset_mod_events
local register_mod_events = hotpatch_tools.register_mod_events
local unload_mod = hotpatch_tools.unload_mod
-- internal callbacks when a mod registers events
local register_event = hotpatch_tools.register_event
local register_nth_tick = hotpatch_tools.register_nth_tick
local register_on_tick = hotpatch_tools.register_on_tick
-- mod bootstrap functions
-- These take a LOADED INDEX as a first argument
local mod_on_init = hotpatch_tools.mod_on_init
local mod_on_load = hotpatch_tools.mod_on_load
local mod_on_configuration_changed = hotpatch_tools.mod_on_configuration_changed
local static_mods = hotpatch_tools.static_mods
local console = hotpatch_tools.console
local debug_log = hotpatch_tools.debug_log
local loaded_mods = hotpatch_tools.loaded_mods
local installed_mods = hotpatch_tools.installed_mods
local help_commands = {
help = '/hotpatch help\nYou got this far, you probably know all you need to know',
list = '/hotpatch list [type]\nLists \'installed\', \'loaded\' or \'running\' mods',
}
local sub_commands
sub_commands = {
help = function(player_index, param)
local caller = (player_index and game.players[player_index]) or console
if (not param) or (param == '') then
caller.print('Usage: /hotpatch [command] [parameters]')
caller.print('Available commands:')
for k, _ in pairs(sub_commands) do caller.print(k) end
caller.print('Use /hotpatch help [command] for help on a particular command')
else
local text = help_commands[param]
if text then
caller.print(text)
end
end
end,
list = function(player_index, param)
local caller = (player_index and game.players[player_index]) or console
if not param or (param == '') then
caller.print('Installed mods:')
for k, v in pairs(installed_mods) do
caller.print(table.concat{'[', k, '] ', v.name, ' ', v.version})
end
caller.print('Loaded/Running mods:')
for k, v in pairs(loaded_mods) do
caller.print(table.concat{'[', k, '] ', v.name, ' ', v.version, ' ', (v.loaded and 'loaded') or 'not loaded', ' ', (v.running and 'running') or 'not running'})
end
else
end
end,
}
local admin_commands = {
reinstall = function(player_index, param)
local caller = (player_index and game.players[player_index]) or console
local mod
local installed_index
local loaded_index
for i = 1, #static_mods do
mod = static_mods[i]
if mod.name:match('^hotpatch%-.*$') then
loaded_index = find_loaded_mod(mod.name)
if loaded_index then
unload_mod(loaded_index)
end
install_mod(mod.name, mod.version, mod.code, mod.files)
installed_index = find_installed_mod(mod.name)
load_mod(installed_index)
loaded_index = find_loaded_mod(mod.name)
run_mod(loaded_index)
mod_on_init(loaded_index)
end
end
end,
}
_ENV.commands.add_command('hotpatch', 'Commands for hotpatch. Run /hotpatch help for details.', function(e)
local caller = (e.player_index and game.players[e.player_index]) or console
if (not e.parameter) or (e.parameter == '') then
sub_commands['help'](e.player_index)
return
end
local sub_command, rest = e.parameter:match('(.-)%s(.*)')
if not sub_command then
sub_command = e.parameter
end
local f = sub_commands[sub_command]
if f then f(e.player_index, rest) end
if caller.admin then
f = admin_commands[sub_command]
if f then f(e.player_index, rest) end
end
end)
--]===])
return true
-377
View File
@@ -1,377 +0,0 @@
local hotpatch_tools = require 'hotpatch.mod-tools'
hotpatch_tools.static_mod('hotpatch-gui', '1.0.6', [===[
--[[
Copyright 2018 Chrisgbk
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
-- MIT License, https://opensource.org/licenses/MIT
local hotpatch_tools = require 'hotpatch.mod-tools'
local mod_gui = require 'mod-gui'
--load private API
-- mod installation/uninstallation support functions
-- These take a mod NAME as a first argument
local install_mod = hotpatch_tools.install_mod
local find_installed_mod = hotpatch_tools.find_installed_mod
local install_mod_file = hotpatch_tools.install_mod_file
local uninstall_mod = hotpatch_tools.uninstall_mod
-- mod interaction functions
-- These take a LOADED INDEX as a first argument, except load_mod, which takes an INSTALLED INDEX
local load_mod = hotpatch_tools.load_mod
local find_loaded_mod = hotpatch_tools.find_loaded_mod
local run_mod = hotpatch_tools.run_mod
local reset_mod = hotpatch_tools.reset_mod
local reset_mod_events = hotpatch_tools.reset_mod_events
local register_mod_events = hotpatch_tools.register_mod_events
local unload_mod = hotpatch_tools.unload_mod
-- internal callbacks when a mod registers events
local register_event = hotpatch_tools.register_event
local register_nth_tick = hotpatch_tools.register_nth_tick
local register_on_tick = hotpatch_tools.register_on_tick
-- mod bootstrap functions
-- These take a LOADED INDEX as a first argument
local mod_on_init = hotpatch_tools.mod_on_init
local mod_on_load = hotpatch_tools.mod_on_load
local mod_on_configuration_changed = hotpatch_tools.mod_on_configuration_changed
local static_mods = hotpatch_tools.static_mods
local console = hotpatch_tools.console
local debug_log = hotpatch_tools.debug_log
local loaded_mods = hotpatch_tools.loaded_mods
local installed_mods = hotpatch_tools.installed_mods
script.on_event(defines.events.on_player_joined_game, function(e)
local player = game.players[e.player_index]
local top = mod_gui.get_button_flow(player)
local left = mod_gui.get_frame_flow(player)
local center = player.gui.center
local button = top['hotpatch-button']
if button then
button.destroy()
end
local menu = left['hotpatch-menu']
if menu then
menu.destroy()
end
local IDE = center['hotpatch-IDE']
if IDE then
IDE.destroy()
end
local main = center['hotpatch-main']
if main then
main.destroy()
end
button = top.add{type = 'sprite-button', name = 'hotpatch-button', sprite='utility/heat_exchange_indication', tooltip = 'Hotpatch', style = mod_gui.button_style}
end)
local on_gui_click_handlers
local on_gui_selection_state_changed_handlers
on_gui_click_handlers = {
['hotpatch-button'] = function(e)
--on_gui_click_handlers['hotpatch-menu.IDE'](e)
--do return end
local player = game.players[e.player_index]
local left = mod_gui.get_frame_flow(player)
local center = player.gui.center
local menu = left['hotpatch-menu']
if not menu then
menu = left.add{type = 'frame', name = 'hotpatch-menu', direction = 'vertical'}
menu.add{type = 'button', name = 'hotpatch-menu.IDE', caption = 'Mod IDE', tooltip = 'Open debugging GUI'}
menu.add{type = 'button', name = 'hotpatch-menu.console', caption = 'Mod Console', tooltip = 'Open debugging console'}
menu.visible = true
return
end
menu.visible = not menu.visible
end,
['hotpatch-menu'] = function(e)
local player = game.players[e.player_index]
local left = mod_gui.get_frame_flow(player)
if e.element.name ~= 'hotpatch-menu' then
local menu = left['hotpatch-menu']
menu.visible = not menu.visible
on_gui_click_handlers[e.element.name](e)
end
end,
['hotpatch-menu.IDE'] = function(e)
local player = game.players[e.player_index]
local center = player.gui.center
local IDE = center['hotpatch-IDE']
if not IDE then
IDE = center.add{type = 'frame', name = 'hotpatch-IDE', direction = 'vertical', caption = 'Hotpatch IDE', style = mod_gui.frame_style}
local top_flow = IDE.add{type = 'flow', name = 'hotpatch-IDE-top', direction = 'horizontal'}
top_flow.add{type = 'label', name = 'hotpatch-IDE-mod-label', caption = 'Mod: '}
local IDE_dropdown = top_flow.add{type = 'drop-down', name = 'hotpatch-IDE-mod-selector'}
top_flow.add{type = 'label', name = 'hotpatch-IDE-mod-version', caption = 'Version: No mod selected'}
local IDE_table = IDE.add{type = 'table', name = 'hotpatch-IDE-table', column_count = 2}
local files = IDE_table.add{type = 'scroll-pane', name = 'hotpatch-IDE-files', direction='vertical'}
files.style.height = 600
files.vertical_scroll_policy = 'always'
files = files.add{type = 'table', name = 'hotpatch-IDE-files-table', column_count = 1}
local code = IDE_table.add{type = 'text-box', name = 'hotpatch-IDE-code'}
code.word_wrap = true
files.style.width = 400
files.style.cell_padding = 0
files.style.top_padding = 0
files.style.bottom_padding = 0
code.style.width = 600
code.style.height = 600
IDE.visible = false
end
local IDE_dropdown = IDE['hotpatch-IDE-top']['hotpatch-IDE-mod-selector']
IDE_dropdown.clear_items()
for _, v in ipairs(installed_mods) do
IDE_dropdown.add_item(v.name)
end
IDE.visible = not IDE.visible
if IDE.visible then
player.opened = IDE
else
player.opened = nil
end
end,
['hotpatch-menu.console'] = function(e)
local player = game.players[e.player_index]
local center = player.gui.center
local hotpatch_console = center['hotpatch-console']
if not hotpatch_console then
hotpatch_console = center.add{type = 'frame', name = 'hotpatch-console', direction = 'vertical', caption = 'Hotpatch Console'}
local top_flow = hotpatch_console.add{type = 'flow', name = 'hotpatch-console-top', direction = 'horizontal'}
top_flow.add{type = 'label', name = 'hotpatch-console-mod-label', caption = 'Mod: '}
local console_dropdown = top_flow.add{type = 'drop-down', name = 'hotpatch-console-mod-selector'}
top_flow.add{type = 'label', name = 'hotpatch-console-mod-version', caption = 'Version: No mod selected'}
local bottom_flow = hotpatch_console.add{type = 'flow', name = 'hotpatch-console-bottom', direction = 'vertical'}
local output = bottom_flow.add{type = 'text-box', name = 'hotpatch-console-output'}
local bottom_input_flow = bottom_flow.add{type = 'flow', name = 'hotpatch-console-bottom-input', direction = 'horizontal'}
local input = bottom_input_flow.add{type = 'textfield', name = 'hotpatch-console-input'}
bottom_input_flow.add{type = 'button', name = 'hotpatch-console-run', caption = 'Run', tooltip = 'Run Lua Code'}
output.style.width = 800
output.style.height = 600
input.style.width = 600
hotpatch_console.visible = false
end
local console_dropdown = hotpatch_console['hotpatch-console-top']['hotpatch-console-mod-selector']
console_dropdown.clear_items()
for _, v in ipairs(loaded_mods) do
console_dropdown.add_item(v.name)
end
hotpatch_console.visible = not hotpatch_console.visible
if hotpatch_console.visible then
player.opened = hotpatch_console
else
player.opened = nil
end
end,
['hotpatch-console-run'] = function(e)
local player = game.players[e.player_index]
local center = player.gui.center
local hotpatch_console = center['hotpatch-console']
local output = hotpatch_console['hotpatch-console-bottom']['hotpatch-console-output']
local input = hotpatch_console['hotpatch-console-bottom']['hotpatch-console-bottom-input']['hotpatch-console-input']
local console_dropdown = hotpatch_console['hotpatch-console-top']['hotpatch-console-mod-selector']
if not player.admin then
output.text = table.concat{output.text, '\n', 'Only admins!'}
return
end
if not (console_dropdown.selected_index > 0) then
output.text = table.concat{output.text, '\n', 'Select a mod!'}
return
end
local mod_name = console_dropdown.items[console_dropdown.selected_index]
local loaded_index = find_loaded_mod(mod_name)
local env = loaded_mods[loaded_index].env
local old_print = env.print
local old_log = env.log
env.print = function(...)
--local count = select('#', ...)
local t = table.pack(...)
for k, v in pairs(t) do
t[k] = tostring(v)
end
local text = table.concat(t, '\t')
output.text = table.concat{output.text, '\n', text}
end
env.log = function(text)
output.text = table.concat{output.text, '\n', text}
end
local success
local code, err = load(input.text, input.text, 't', env)
if code then
success, err = pcall(code)
if not success then
output.text = table.concat{output.text, '\n', err}
end
else
output.text = table.concat{output.text, '\n', err}
end
env.print = old_print
env.log = old_log
end,
['hotpatch-IDE-file'] = function(e)
local player = game.players[e.player_index]
local center = player.gui.center
local element = e.element
local previous
for _, v in pairs(center['hotpatch-IDE']['hotpatch-IDE-table']['hotpatch-IDE-files']['hotpatch-IDE-files-table'].children) do
if table.compare(v.style.font_color or {}, {r=1.0, g=1.0, b=0.0, a=1.0}) then
previous = v
break
end
end
if previous then
previous.style.font_color = {r=1.0, g=1.0, b=1.0, a=1.0}
end
element.style.font_color = {r=1.0, g=1.0, b=0.0, a=1.0}
local file = element.name:match('.-%.(.*)')
local selected = center['hotpatch-IDE']['hotpatch-IDE-top']['hotpatch-IDE-mod-selector']
local mod_name = selected.items[selected.selected_index]
local code = center['hotpatch-IDE']['hotpatch-IDE-table']['hotpatch-IDE-code']
local index = find_installed_mod(mod_name)
if index then
local mod = installed_mods[index]
if file == 'control' then
code.text = mod.code
else
code.text = mod.files[file]
end
end
end,
}
script.on_event(defines.events.on_gui_click, function(e)
local element = e.element
if element.valid then
local name = element.name:match('([^%.]*)%.?.-')
local handler = on_gui_click_handlers[name]
if handler then handler(e) end
end
end)
on_gui_selection_state_changed_handlers = {
['hotpatch-console-mod-selector'] = function(e)
local player = game.players[e.player_index]
local center = player.gui.center
local element = e.element
local name = element.items[element.selected_index]
if name then
local index = find_loaded_mod(name)
if index then
local mod = loaded_mods[index]
local version_label = center['hotpatch-console']['hotpatch-console-top']['hotpatch-console-mod-version']
version_label.caption = 'Version: ' .. mod.version
local hotpatch_console = center['hotpatch-console']
local output = hotpatch_console['hotpatch-console-bottom']['hotpatch-console-output']
output.text = ''
end
end
end,
['hotpatch-IDE-mod-selector'] = function(e)
local player = game.players[e.player_index]
local center = player.gui.center
local element = e.element
local name = element.items[element.selected_index]
if name then
local index = find_installed_mod(name)
if index then
local mod = installed_mods[index]
local version_label = center['hotpatch-IDE']['hotpatch-IDE-top']['hotpatch-IDE-mod-version']
version_label.caption = 'Version: ' .. mod.version
local list = center['hotpatch-IDE']['hotpatch-IDE-table']['hotpatch-IDE-files']['hotpatch-IDE-files-table']
list.clear()
local file = list.add{type = 'label', style = 'hoverable_bold_label', caption = 'control', name = 'hotpatch-IDE-file.control'}
file.style.bottom_padding = 0
file.style.top_padding = 0
for k, _ in pairs(mod.files) do
file = list.add{type = 'label', style = 'hoverable_bold_label', caption = k, name = 'hotpatch-IDE-file.' .. k}
file.style.bottom_padding = 0
file.style.top_padding = 0
end
local code = center['hotpatch-IDE']['hotpatch-IDE-table']['hotpatch-IDE-code']
code.text = ''
end
end
end,
}
script.on_event(defines.events.on_gui_selection_state_changed, function(e)
local element = e.element
local handler = on_gui_selection_state_changed_handlers[element.name]
if handler then handler(e) end
end)
script.on_event(defines.events.on_gui_closed, function(e)
local player = game.players[e.player_index]
local element = e.element
if e.gui_type == defines.gui_type.custom then
if element and element.valid then
if element.name == 'hotpatch-IDE' then
if element.visible then
player.play_sound{path = "utility/gui_click"}
end
element.visible = false
elseif element.name == 'hotpatch-console' then
if element.visible then
player.play_sound{path = "utility/gui_click"}
end
element.visible = false
end
end
end
end)
--]===])
--[===[
-- keeping this as a reminder
['hotpatch-menu'] = function(e)
local player = game.players[e.player_index]
local top = mod_gui.get_button_flow(player)
local left = mod_gui.get_frame_flow(player)
local center = player.gui.center
local menu = left['hotpatch-menu']
menu.visible = not menu.visible
on_gui_click_handlers[e.element.name](e)
end,
--]===]
return true
File diff suppressed because it is too large. Load diff
@@ -1,322 +0,0 @@
local hotpatch_tools = require 'hotpatch.mod-tools'
hotpatch_tools.static_mod('hotpatch-remote-interface', '1.0.3', [===[
--[[
Copyright 2018 Chrisgbk
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
-- MIT License, https://opensource.org/licenses/MIT
local hotpatch_tools = require 'hotpatch.mod-tools'
--load private API
-- mod installation/uninstallation support functions
-- These take a mod NAME as a first argument
local install_mod = hotpatch_tools.install_mod
local find_installed_mod = hotpatch_tools.find_installed_mod
local install_mod_file = hotpatch_tools.install_mod_file
local uninstall_mod = hotpatch_tools.uninstall_mod
-- mod interaction functions
-- These take a LOADED INDEX as a first argument, except load_mod, which takes an INSTALLED INDEX
local load_mod = hotpatch_tools.load_mod
local find_loaded_mod = hotpatch_tools.find_loaded_mod
local run_mod = hotpatch_tools.run_mod
local reset_mod = hotpatch_tools.reset_mod
local reset_mod_events = hotpatch_tools.reset_mod_events
local register_mod_events = hotpatch_tools.register_mod_events
local unload_mod = hotpatch_tools.unload_mod
-- internal callbacks when a mod registers events
local register_event = hotpatch_tools.register_event
local register_nth_tick = hotpatch_tools.register_nth_tick
local register_on_tick = hotpatch_tools.register_on_tick
-- mod bootstrap functions
-- These take a LOADED INDEX as a first argument
local mod_on_init = hotpatch_tools.mod_on_init
local mod_on_load = hotpatch_tools.mod_on_load
local mod_on_configuration_changed = hotpatch_tools.mod_on_configuration_changed
local static_mods = hotpatch_tools.static_mods
local console = hotpatch_tools.console
local debug_log = hotpatch_tools.debug_log
local loaded_mods = hotpatch_tools.loaded_mods
local installed_mods = hotpatch_tools.installed_mods
local remote_interface = {}
--IMPORTANT: WHEN COPY PASTING CODE TO CONSOLE THE CODE MUST HAVE SINGLE LINE COMMENTS REMOVED
-- FACTORIO CONSOLE STRIPS LINEFEEDS WHICH MAKES ALL THE CODE BECOME COMMENTED OUT
--TODO: gate these behind admin permissions; if (game.player and game.player.admin) should work for console commands?
debug_log({'hotpatch-info.remote-installing'})
remote_interface['install'] = function(mod_name, mod_version, mod_code, mod_files, only_install)
-- this installs a new mod and runs on_init, then registers events
-- Note that mods may expect that certain events haven't been called yet when their on_init is ran
-- This may prevent them from functioning properly, without manually calling the events they expect
-- examples: on_player_created
local caller = game.player or console
if caller.admin then
if mod_files then
for k,_ in pairs(mod_files) do
if k == 'control' then
caller.print('ERROR: control.lua loaded twice for mod ' .. mod_name)
return
end
end
end
local installed_index = find_installed_mod(mod_name)
local loaded_index = find_loaded_mod(mod_name)
if installed_index then
local old_version = installed_mods[installed_index].version
if old_version then
debug_log({'hotpatch-warning.already-exists', mod_name, old_version})
debug_log({'hotpatch-warning.reinstalling', mod_name, old_version})
end
if loaded_index then
unload_mod(loaded_index)
end
end
install_mod(mod_name, mod_version, mod_code, mod_files)
if not only_install then
installed_index = find_installed_mod(mod_name)
if installed_index then
if not load_mod(installed_index) then
caller.print('compilation failed for mod ' .. mod_name)
return
end
loaded_index = find_loaded_mod(mod_name)
if loaded_index then
if not run_mod(loaded_index) then
caller.print('execution failed for mod ' .. mod_name)
unload_mod(loaded_index)
return
end
if not mod_on_init(loaded_index) then
caller.print('on_init failed for mod ' .. mod_name)
unload_mod(loaded_index)
return
end
-- TODO: notify all mods
-- TODO: determine vanilla behaviour and replicate it
if not mod_on_configuration_changed(loaded_index, {mod_changes = {mod_name={new_version=mod_version}}}) then
caller.print('on_configuration_changed failed for mod ' .. mod_name)
unload_mod(loaded_index)
return
end
else
end
else
end
end
else
caller.print('You must be an admin to run this command.')
end
end
remote_interface['run'] = function(mod_name)
-- this runs on_init, then registers events
-- Note that mods may expect that certain events haven't been called yet when their on_init is ran
-- This may prevent them from functioning properly, without manually calling the events they expect
-- examples: on_player_created
local caller = game.player or console
if caller.admin then
local installed_index = find_installed_mod(mod_name)
local loaded_index = find_loaded_mod(mod_name)
local mod
local version
if installed_index and not loaded_index then
if not load_mod(installed_index) then
caller.print('compilation failed for mod ' .. mod_name)
return
end
loaded_index = find_loaded_mod(mod_name)
else
if loaded_index then
mod = loaded_mods[loaded_index]
version = mod.version
debug_log({'hotpatch-warning.already-loaded', mod_name, version})
debug_log({'hotpatch-warning.reinitializing', mod_name, version})
end
end
if loaded_index then
if not run_mod(loaded_index) then
caller.print('execution failed for mod ' .. mod_name)
unload_mod(loaded_index)
return
end
if not mod_on_init(loaded_index) then
caller.print('on_init failed for mod ' .. mod_name)
unload_mod(loaded_index)
return
end
-- TODO: notify all mods
-- TODO: determine vanilla behaviour and replicate it
if not mod_on_configuration_changed(loaded_index, {mod_changes = {mod_name={new_version=version}}}) then
caller.print('on_configuration_changed failed for mod ' .. mod_name)
unload_mod(loaded_index)
return
end
end
else
caller.print('You must be an admin to run this command.')
end
end
remote_interface['update'] = function(mod_name, mod_version, mod_code, mod_files)
-- this updates an existing mod
-- the current mods events are de-registered, the new code is installed, on_load is triggered, and then events are registered
-- finally, the mod is informed of the update, so it can run migrations from the previous version
-- TODO: validation
local caller = game.player or console
if caller.admin then
if mod_files then
for k,_ in pairs(mod_files) do
if k == 'control' then
caller.print('ERROR: control.lua loaded twice for mod ' .. mod_name)
return
end
end
end
local installed_index = find_installed_mod(mod_name)
if installed_index then
;
end
local loaded_index = find_loaded_mod(mod_name)
local old_version
if loaded_index then
old_version = loaded_mods[loaded_index].version
unload_mod(loaded_index)
end
install_mod(mod_name, mod_version, mod_code, mod_files)
installed_index = find_installed_mod(mod_name)
if not load_mod(installed_index) then
caller.print('compilation failed for mod ' .. mod_name)
return
end
loaded_index = find_loaded_mod(mod_name)
if not run_mod(loaded_index) then
caller.print('execution failed for mod ' .. mod_name)
unload_mod(loaded_index)
return
end
if old_version then
if not mod_on_load(loaded_index) then
caller.print('on_load failed for mod ' .. mod_name)
unload_mod(loaded_index)
return
end
-- The mod must do any migrations here
-- TODO: notify all mods
if not mod_on_configuration_changed(loaded_index, {mod_changes = {mod_name={old_version=old_version, new_version=mod_version}}}) then
caller.print('on_configuration_changed failed for mod ' .. mod_name)
unload_mod(loaded_index)
return
end
else
-- first time install
if not mod_on_init(loaded_index) then
caller.print('on_init failed for mod ' .. mod_name)
unload_mod(loaded_index)
return
end
-- TODO: notify all mods
if not mod_on_configuration_changed(loaded_index, {mod_changes = {mod_name={new_version=mod_version}}}) then
caller.print('on_configuration_changed failed for mod ' .. mod_name)
unload_mod(loaded_index)
return
end
end
else
caller.print('You must be an admin to run this command.')
end
end
remote_interface['install_mod_file'] = function(mod_name, mod_file, mod_file_code)
local caller = game.player or console
if caller.admin then
install_mod_file(mod_name, mod_file, mod_file_code)
else
caller.print('You must be an admin to run this command.')
end
end
remote_interface['exec'] = function(mod_name, exec_code)
local caller = game.player or console
if caller.admin then
local loaded_index = find_loaded_mod(mod_name)
local env = loaded_mods[loaded_index].env
local success
local code, err = load(exec_code, exec_code, 't', env)
if code then
success, err = pcall(code)
if not success then
debug_log(err)
end
else
debug_log(err)
end
else
caller.print('You must be an admin to run this command.')
end
end
--TODO: most of this function
remote_interface['clean'] = function(mod_name)
-- Removes ALL mod data and reinitializes
-- doesn't remove things a mod may add like surfaces, etc
local caller = game.player or console
if caller.admin then
--mod_reset(mod_name)
--run_mod(mod_name)
--mod_on_init(mod_name)
local loaded_index = find_loaded_mod(mod_name)
local mod = loaded_mods[loaded_index]
local version = mod.version
mod_on_configuration_changed(loaded_index, {mod_changes = {mod_name={new_version=version}}})
else
caller.print('You must be an admin to run this command.')
end
end
remote_interface['uninstall'] = function(mod_name)
-- Uninstalls a mod
local caller = game.player or console
if caller.admin then
uninstall_mod(mod_name)
else
caller.print('You must be an admin to run this command.')
end
end
remote.add_interface('hotpatch', remote_interface)
debug_log({'hotpatch-info.complete', {'hotpatch-info.remote-installing'}})
--]===])
--[[
remote.call('hotpatch, 'install', 'test', '1.0.0', [===[
script.on_event(defines.events.on_player_changed_position, function(e)
game.print('changed position')
end
]===]
local updated_code = ....
remote.call('hotpatch', 'update', 'test', '1.0.1', updated_code)
]]
return true
@@ -1,3 +0,0 @@
msg-intro=Hierdie is Factorio vryspel. Jou taak is om 'n vuurpyl in die ruimte in te stuur. Doen dit deur 'n vuurpyl te bou en hom te lanseer. Jy sal baie gevorderde tegnologiëe moet navors om die Vuurpyl Silo te ontsluit. Begin klein, werk jou pad op met outomatisering en moenie vergeet om jouself te beskerm teen die inboorlinge nie.
scenario-name=Vryspel
@@ -1,3 +0,0 @@
msg-intro=.بكوكلا اذه ناكس م كسفن يمحت نا ىسنت الو ,ةيكيتاموتوألا ربع كقيرط ينبتسو ,رفصلا نم أدبتس .كلت خيراوصلا قالطإ ةطحم حتف عيطتست فيكل ةروطتملا ايجولونكتلل ثاحبأب مايقلا ىلإ جاتحتس .اهنم خوراص قالطإ مث خيراوص قالطإ ةطحم ءانب قيرط نع كلذب مايقلا كنكمي ,ءاضفلا ىلإ اخوراص قلطت نا يه كتمهم ,factorioـل رحلا بعللا روط وه اذه
scenario-name=رحلا بعللا
@@ -1,3 +0,0 @@
msg-intro=Гэта рэжым свабоднай гульні Factorio. Вашая мэта - запусціць ракету ў космас. Дзеля гэтага вам трэба пабудаваць ракетную шахту і саму ракету са спадарожнікам. Вы мусіце даследаваць перадавыя тэхналогіі, каб навучыцца будаваць ракетную шахту. Пачынайце з малога, працуйце над аўтаматызацыяй вытворчасці, даследуйце новыя тэхналогіі і не забывайцеся на абарону ад «тутэйшых».
scenario-name=Свабодная гульня
@@ -1,3 +0,0 @@
msg-intro=Това е режимът на свободна игра. Задачата ви е да изстреляте ракета в космоса. За това ще е нужно да построите ракетна шахта и изстреляте ракетата със спътник. Ще трябва да проучите развити технологии, които ще отключат ракетната шахта. Започнете с малко и постепенно автоматизирайте процеса, като не забравяте да се защитавате от местните форми на живот.
scenario-name=Свободна игра
@@ -1,3 +0,0 @@
msg-intro=Aquest és el mode lliure del Factorio. La vostra tasca és construir un coet i enviar-lo a l'espai. Ho haureu de fer construint un centre de coets espacials i llençant un coet. Necessitareu investigar tecnologies avançades per poder-ho fer. Aneu pas a pas, avanceu cap a l'automatització i no oblideu protegir-vos dels natius.
scenario-name=Mode lliure
@@ -1,3 +0,0 @@
msg-intro=Toto je volná hra Factorio. Tvým úkolem je vypustit raketu do vesmíru. Postav raketové silo a vypusť raketu. Pro odemknutí raketového sila budeš muset vyzkoumat pokročilé technologie. Začni v malém, postupuj s automatizací a nezapomeň se chránit před domorodci.
scenario-name=Volná hra
@@ -1,3 +0,0 @@
msg-intro=Dette er Factorio freeplay. Din opgave er at affyre en raket ud i rummet. Gør dette ved at konstruere en raketsilo og affyr en raket. Du bliver nød til at undersøge avancerede teknologier for at låse op for raketsiloen. Start småt og arbejd din vej op med automatisering og glem ikke at beskytte dig selv fra de indfødte.
scenario-name=Fri lej
@@ -1,3 +0,0 @@
msg-intro=Du befindest dich im freien Spiel Modus von Factorio. Ziel ist es, eine Rakete ins Weltall zu schicken. Baue hierzu das Raketensilo um die Rakete starten zu können. Um das Raketensilo freischalten zu können wirst du fortgeschrittene Technologien erforschen müssen. Fange klein an und erforsche die Automation. Vergiss aber nicht, dich vor den Ureinwohner zu schützen.
scenario-name=Freies Spiel
@@ -1,3 +0,0 @@
msg-intro=Αυτό είναι το ελεύθερο παιχνίδι του Factorio. Ο στόχος σας είναι να εκτοξεύσετε έναν πύραυλο στο διάστημα. Αυτό το κάνετε κατασκευάζοντας ένα σιλό πυραύλου και εκτοξεύοντας έναν πύραυλο. Θα πρέπει να ερευνήσετε προηγμένες τεχνολογίες για να ξεκλειδώσετε το σιλό πυραύλου. Ξεκινήστε χαμηλά, φτάστε ψηλά χρησιμοποιώντας αυτοματισμούς και μην ξεχάσετε να προστατέψετε τον εαυτό σας από τους ντόπιους.
scenario-name=Ελεύθερο παιχνίδι
@@ -1,2 +0,0 @@
msg-intro=This is the Factorio freeplay. Your task is to launch a rocket into space. Do this by constructing a rocket silo and launching a rocket. You will need to research advanced technologies in order to unlock the rocket silo. Start small, work your way up with automation and don't forget to protect yourself from the natives.
scenario-name=Freeplay
@@ -1,81 +0,0 @@
[hotpatch]
log=@__1__:__2__] __3__
log-mod=@__1__:__2__][__4__] __3__
severe=SEVERE ERROR: __1__
error=ERROR: __1__
warning=WARNING: __1__
info=INFO: __1__
verbose=INFO: __1__
trace=TRACE: __1__
[hotpatch-info]
logging-enabled=Logging enabled
metatable-installed=_ENV metatable installed
complete=__1__ Complete!
on-init=initializing...
on-load=loading...
on-configuration-changed=mod configuration changed...
installing-included-mods=installing included mods...
loading-installed-mods=loading installed mods...
loading-libs=loading Factorio.data.core.lualib...
loading-library=loading library: __1__
uninstalling=Uninstalling mod...
installing=Installing version __1__...
installing-file=Installing file __1__...
script-shim=setting up mod script shim...
setting-env=setting up mod _ENV...
loading=loading...
unloading=unloading...
running=running...
must-be-admin=You must be an admin to run this command
remote-installing=installing remote interface
[hotpatch-trace]
nil-var-access=_ENV nil variable access: __1__
nil-var-assignment=_ENV variable assignment: __1__
event-registering=registering events...
on-tick-event-registered=registered on_tick event...
on-event-registered=registered event __1__...
on-nth-tick-event-registered=registered nth_tick event __1__...
event-running=running event __1__...
nth-tick-event-running=running nth_event __1__...
mod-on-init=running on_init...
mod-on-load=running on_load...
mod-on-configuration-changed=running on_configuration_changed...
adding-event=adding event __1__
adding-nth-tick-event=adding nth_tick event __1__
caching-event=caching event: __1__...
caching-nth-tick-event=caching nth_tick event: __1__...
event-processing=processing event __1__...
nth-tick-event-processing=processing nth_tick event __1__...
nth-tick-handler-added=added nth_tick handler: __1__
nth-tick-handler-removed=removed nth_tick handler: __1__
on-tick-handler-added=added on_tick handler
on-tick-handler-removed=removed on_tick handler
on-event-handler-added=added event handler: __1__
on-event-handler-removed=removed event handler: __1__
cached-load-require=loading cached require'd file: __1__...
load-require=loading require'd file: __1__...
load-core-lib=loading from Factorio.data.core.lualib: __1__...
[hotpatch-warning]
contains-comments=mod code contains comments!
contains-comments-no-lf=mod contains comments and no linefeed!
contains-comments-console=comments from console will comment out the entire code!
reset-events-not-running=tried to reset events for mod that isn't running!
remote-interface-exists=remote interface __1__ already exists, removing...
command-exists=command __1__ already exists, removing...
already-exists=mod already exists: __1__ __2__
reinstalling=reinstalling mod in-place: __1__ __2__
already-loaded=mod already loaded: __1__ __2__
reinitializing=reinitializing: __1__ __2__
[hotpatch-error]
invalid-API-access=Invalid API access: __1__
not-installed=mod not installed, cannot install file for mod that does not exist
compilation-failed=compilation failed for mod
execution-failed=execution failed for mod
path-not-found=path '__1__' not found
on-init-failed=on_init failed
on-load-failed=on_load failed
on-configuration-changed-failed=on_configuration_changed failed
@@ -1,2 +0,0 @@
scenario-name=Libera Ludado
@@ -1,3 +0,0 @@
msg-intro=Este es el modo de juego libre de Factorio. Tu tarea es la de lanzar un cohete al espacio. Logra esto construyendo un Silo de Cohetes y lanzando un cohete con un satélite. Necesitarás investigar tecnologías avanzadas para poder desbloquear el Silo de Cohetes. Comienza poco a poco, trabaja en tu sistema de automatización y no te olvides de protegerte de los nativos.
scenario-name=Modo libre
Loaded 100 of 4028 files, more files were not shown because too many files have changed in this diff. Show more