Compare commits

...
Author SHA1 Message Date
Cooldude2606 4e9df0bc21 v2.0.0-alpha.20 2025-02-28 23:55:11 +00:00
Cooldude2606 2c44890100 Move package release to workflow docs 2025-02-28 23:34:24 +00:00
Daniel febdbd4fd6 Document release workflow (#738) 2025-02-28 23:11:59 +00:00
Danielv123 d622ab5127 Fix missing key on users table roles column
Results in inconsistent visual behaviour when creating and deleting roles
2025-02-27 16:13:33 +01:00
Danielv123 a893109a06 Fix NaN in subspace storage database 2025-02-27 00:05:48 +01:00
Cooldude2606 9f888867fe Dep version bumps
Temp Fix #737
2025-02-25 21:09:11 +00:00
Danielv123 12f833fcc3 Add clusterio_lib release changelog 2025-02-25 19:14:32 +01:00
Daniel a16dcd315f Merge pull request #733 from clusterio/feature/subspace-storage-space-age
Feature/subspace storage space age
2025-02-25 18:53:36 +01:00
Danielv123 9cd68fcb36 Bump version number 2025-02-25 18:39:05 +01:00
Danielv123 ebafda5407 Update with comments from review 2025-02-25 16:33:52 +01:00
Danielv123 a9e1bdcc65 Test itemDatabase with quality and migrations from pre quality format 2025-02-16 14:44:49 +01:00
Danielv123 f907455620 Validate quality parameter 2025-02-16 14:44:15 +01:00
Danielv123 19336cdd3b Remove test dependency on old version of subspace mod 2025-02-16 14:30:47 +01:00
Danielv123 5d37712db4 Fix online time export in 2.0 w backwards compat 2025-02-16 13:34:35 +01:00
Cooldude2606 8996671bea Add default mod packs (#735) 2025-02-14 14:47:26 +00:00
Daniel 7b8945ff15 Don't write input stats to both input and output (#734) 2025-02-14 14:47:13 +00:00
Danielv123 fbdf206e31 Test: Don't depend on a specific version of clusterio_lib 2025-01-25 17:55:43 +01:00
Danielv123andCooldude2606 edfe32c59e Simplify checks based on review comments
Co-authored-by: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com>
2025-01-25 17:39:47 +01:00
Danielv123 f78966860e Build clusterio lib with CI 2025-01-24 19:23:15 +01:00
Danielv123 2162351710 Bump clusterio_lib versions along with subspace 2025-01-24 17:49:04 +01:00
Danielv123 a33aa3b7e5 Show quality in web UI 2025-01-23 18:54:06 +01:00
Danielv123 e8570029b0 Send and return requests 2025-01-23 00:04:00 +01:00
Danielv123 4fd123abc1 Add quality support to subspace storage 2025-01-22 19:46:23 +01:00
Cooldude2606 26269df7d4 Add 2.0.29 freeplay hash
Hot patch to add the updated 2.0.29 control.lua freeplay hash to knoww scenarios.
2025-01-21 20:43:00 +00:00
phidias 52d8822048 Fix human readable time format (#732) 2025-01-20 11:12:34 +00:00
Daniel 97e25eb885 Handle missing logistic requester point (#731) 2025-01-20 11:11:47 +00:00
Cooldude2606 fa1338ddd4 Add decisions log (#714)
* Add decisions log

* Response to Daniel's comments

* Add PR Links
2025-01-13 21:30:06 +00:00
Cooldude2606 4d66dc5de3 v2.0.0-alpha19.b
Add missing prepare step for web_ui package.
2024-12-24 20:26:31 +00:00
43 changed files with 1031 additions and 327 deletions

No files matched your search

+20
View File
@@ -55,3 +55,23 @@ jobs:
node-version: 18.x
- run: pnpm i --no-frozen-lockfile
- run: pnpm run lint
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: 18.x
- run: pnpm i --no-frozen-lockfile
- run: pnpm run build-mod
# Upload build to artifacts
- name: Upload build to artifacts
uses: actions/upload-artifact@v4
with:
name: clusterio_lib
path: ./dist
if-no-files-found: error
+32 -1
View File
@@ -13,16 +13,47 @@
### Changes
- Bugfixes and small changes to existing behaviour.
- Small changes to existing behaviour.
### Fixes
- Bugfixes
### Breaking Changes
- Changes which break backwards compatibility with older hosts or plugins code.
### Meta
- Changes to the project activities / structure worth highlighting but have no impact on the published package.
Many thanks to the following for contributing to this release:
[@username](https://github.com/username)
-->
## Version 2.0.0-alpha.20
### Features
- Update subspace storage to 2.0 and space age. [#733](https://github.com/clusterio/clusterio/pull/733)
- A fresh install of Clusterio will include default mod packs. [#719](https://github.com/clusterio/clusterio/issues/719)
### Fixes
- Fix crash when leaving a server without a logistic requester point [#731](https://github.com/clusterio/clusterio/pull/731)
- Use floor instead of round to prevent "1 minute and 60 seconds" being shown [#723](https://github.com/clusterio/clusterio/issues/723)
- Fixed production statistics overwriting consumption statistics and vise versa [#734](https://github.com/clusterio/clusterio/pulls/734)
- Hot patch to add the updated 2.0.29 control.lua freeplay hash to know scenarios. [commit](https://github.com/clusterio/clusterio/commit/26269df7d4d8f39042c59653008bc8f034a4f3b4)
### Meta
- We have now introduced a new [decisions log](https://github.com/clusterio/clusterio/blob/master/docs/decisions.md) to aid tracking all the technical and design decisions we make and their reasons. [#714](https://github.com/clusterio/clusterio/pull/714)
Many thanks to the following for contributing to this release:
[@Cooldude2606](https://github.com/Cooldude2606)
[@Danielv123](https://github.com/Danielv123)
[@psihius](https://github.com/psihius)
## Version 2.0.0-alpha.19
### Major Features
+3 -3
View File
@@ -90,13 +90,13 @@ Check out the documentation on [Writing Plugins](/docs/writing-plugins.md) for w
## Installation
Clusterio runs on Node.js v18 and up, it's distributed via npm and comes with a guided install script.
Clusterio runs on all LTS version of NodeJS, it is distributed via npm and comes with a guided install script.
If you already have a recent Node.js installed, you can set it up in a new directory with:
npm init "@clusterio"
Otherwise see below for OS specific instructions.
Otherwise see below for OS specific instructions.
Check out all our of decisions for [node, factorio, and operation support](/docs/decisions.md#supportability).
### Ubuntu setup
+11 -43
View File
@@ -3,6 +3,8 @@
You are probably reading this because you want to contribute code to Clusterio.
Great! We need people like you.
Though we have some conventions and workflows that we kindly ask you to follow.
We have included all the pratical considerations in this document.
Although, we recomend checking out out our [past decisions](/docs/decisions.md) if you have the time.
## Contents
@@ -15,6 +17,7 @@ Though we have some conventions and workflows that we kindly ask you to follow.
- [Editing your Feature Branch](#editing-your-feature-branch)
- [Resolving Merge Conflicts](#resolving-merge-conflicts)
- [Updating Your GitHub Fork](#updating-your-github-fork)
- [Publishing releases to npm](#publishing-releases-to-npm)
- [Changelog](#changelog)
- [Supported Node.js and Factorio Version](#supported-node.js-and-factorio-version)
- [Code Style](#code-style)
@@ -23,7 +26,6 @@ Though we have some conventions and workflows that we kindly ask you to follow.
- [Indenting](#indenting)
- [Line Length](#line-length)
- [Markdown](#markdown)
- [Pushing releases to npm](#pushing-releases-to-npm)
## Workflow
@@ -178,11 +180,15 @@ Fetch the main repository directly as described above and base your feature bran
GitHub has no interface or functionality for keeping the branches in your fork up to date with the branches in the main repository and there is no point in doing this for our feature branch based workflow.
### Publishing releases to npm
For publishing new releases see [package release workflow](./release-workflow.md#packages).
## Changelog
There's a [Changelog](../CHANGELOG.md) in the root of the project.
If you make changes that are visible to users of this project you should add an entry to the changelog describing what has changed.
The format of this log should be self explanatory, please follow it carefully.
The format of this log should be self explanatory, please follow it carefully. Please make sure to document any [breaking changes](/docs/decisions.md#breaking-changes).
## Supported Node.js and Factorio Versions
@@ -194,12 +200,14 @@ For Factorio Clusterio 2.0 aims to support version 0.17.69 and up, including the
It's recommended that you use the lua API reference for 0.17.69, as there's no information on what version Factorio API's were introduced in.
The GitHub Actions tests are run against the latest experimental release.
Check out all our of decisions for [node, factorio, and operation support](/docs/decisions.md#supportability).
## Code Style
The style of the code is a bit of a mixed bag at the moment.
But there are at least a few things that have been agreed upon.
See our [decisions document](/docs/decisions.md) for justifcations.
### Naming style
@@ -284,43 +292,3 @@ And there's two spaces before the next heading.
Headings use the `# heading` format.
```
## Pushing releases to npm
The releases are done by updating packages versions and then publishing them with pnpm.
You will need commit access to the repository as well publish access to the @clusterio org on npm to do this.
1. To do a release switch to the master branch and make sure it's up to date with the repository before proceeding.
```sh
git switch master
git pull --ff-only upstream master
```
Check and make sure there are no untracked files with `git status`.
2. Check all PRs merged since the last version and copy their change logs into the main changelog.
3. Update the version in package files.
Make sure to update the command for the right version jump.
```sh
sed -i 's/2\.0\.0-alpha\.12/2\.0\.0-alpha\.13/' {packages,plugins}/*/package.json
```
4. Inspect, commit and tag the changes.
Again, make sure to be tagging the right version.
```sh
git add -p
git commit -m v2.0.0-alpha.13
git tag v2.0.0-alpha.13
```
5. Publish the packages.
```sh
pnpm clean
pnpm -r --filter='./{plugins,packages}/*' publish
```
+479
View File
@@ -0,0 +1,479 @@
# Decisions
This document outlines key decisions taken by the project team that should be followed by all maintainers and contributors.
If there is a particular issue or concern you would like the team to comment on then please create a github issue for it.
<!-- Try to use categories that already exist if possible -->
<!-- Sublists should be alphabetical, avoiding starting with "the" -->
<!-- For code style, add (Lang) to section names if present in both -->
1. Documentation
- [Decision Log](#decision-log)
- [TSDoc](#tsdoc)
2. Code Style (General)
- [Indenting](#indenting)
- [Line Length](#line-length)
- [Naming Booleans](#naming-booleans)
- [Naming Files](#naming-files)
- [Naming Times and Durations](#naming-times-and-durations)
- [Strings](#strings)
3. Code Style (TS / JS / Web)
- [ESLint](#eslint)
- [Imports Within `lib` Package](#imports-within-lib-package)
- [Name Style](#name-style-ts)
4. Code Style (Lua)
- [FMTK](#fmtk)
- [Name Style](#name-style-lua)
5. Code Style (Other)
- [Markdown](#markdown)
6. Repository Structure
- [External Plugins](#external-plugins)
- [First Party Plugins](#first-party-plugins)
- [Core Packages](#core-packages)
- [TypeScript Configs](#typescript-configs)
- [Workspace](#workspace)
7. Project Architecture
- [Deprecation of Phin in favour of `fetch`](#deprecation-of-phin-in-favour-of-fetch)
- [Minimal Dependencies](#minimal-dependencies)
- [React](#react)
- [Separation of Host and Controller](#separation-of-host-and-controller)
- [Typescript](#typescript)
- [Webpack](#webpack)
8. Supportability
- [Breaking Changes](#breaking-changes)
- [Distribution](#distribution)
- [Factorio](#factorio)
- [General Support](#general-support)
- [NodeJS](#nodejs)
- [Operating System](#operating-systems)
9. Testing
- [Continuous Integration](#continuous-integration)
- [Continuous Deployment](#continuous-deployment)
- [Mocha](#mocha)
- [Version Matrix](#version-matrix)
> [!note]
> The use of `Time Immemorial` indicates an obsverion of a previously undocumented decision with no known start date, which has remained unchallenged.
<!--
### Template
🧾 0th Month 2000 | Source
> A short sentence describing the action to take in the context of our project.
Write a breif description which includes: the reason a decision was needed, the outcome of the discussion, and the justification of the outcome.
Write using the active voice in third person "we".
Remember to have one sentence per line.
-->
## Documentation
### Decision Log
🧾 22nd December 2024 | Discord Voice Call
> Maintain a document that includes a brief description of the outcomes and the reasoning behind our decisions, along with links to the relevant sources for further reading.
We discovered that we were beginning to ask similar questions to other maintainers that we had previously raised, which led to a waste of time for all involved.
Consequently, we decided that a log of important decisions should be kept for the benefit of ourselves and future maintainers.
### TSDoc
🧾 12th August 2023 | [Discord Discussion](https://discord.com/channels/450361298220351489/450361298220351491/1320573126731370506) | [Github PR #493](https://github.com/clusterio/clusterio/pull/493)
> When a function or it's arguments do not have a clear purpose, a TSDoc comment should be used to describe it.
While TypeScript provides useful type infomation in function hover text, it does not allow for comments about the purpose of functions and their arguments.
Therefore, we decided to use TSDoc comments for this purpose as it is well intergrated in many text editors and IDEs (such as VSCode).
Our specific implementation is TypeDoc because it is lightweight it and can output TSDoc comments as html.
API Extractor was also considered but not selected.
## Code Style (General)
### Indenting
🧾 Time Immemorial
> Use tabs for indention. Except for Markdown where spaces carry meaning.
We needed to maintain a consistent whitespace style across all our files.
Therefore, we selected tabs as our standard.
### Line Length
🧾 Time Immemorial
> Lines are limited to 120 characters where tabs count as 4 characters.
We needed to maintain a consistent max line legnth across all our files.
Therefore, we selected 120 characters as our standard.
### Naming Booleans
🧾 25th November 2024 | [Discord Discussion](https://discord.com/channels/450361298220351489/450361298220351491/1310611960508977223)
> Variable and member properties which contain booleans should be named starting with a verb unless it ends with "ed", e.g. `canRestart` `expectEmpty` `hasFallback` `connected` `enabled` `loaded` `banned`
We had inconsistencies in our naming of booleans and aimed to standardise them.
There was little preference for any particular approach, so we chose the one that would require the fewest changes to existing code.
As a result, all boolean values should begin with a verb, unless they end with "ed." For clarity, both "is" and "has" are considered verbs.
### Naming Files
🧾 Time Immemorial
> Files use lowercase_underscore if multiple values are exported. If a single class is exported then it uses the class name in PascalCase.
We needed to maintain a consistent name style across all our files.
Therefore, we selected lowercase undercore for most files, with class files using PascalCase.
### Naming Times and Durations
🧾 18th January 2024 | [Discord Discussion](https://discord.com/channels/450361298220351489/450361298220351491/1197566542548828170)
> Variable and member properties which contain times or durations should end with their SI unit of messure, e.g. `updatedAtMs`
We found verifying the units of measurement used in different locations to be a simple yet repetitive task that could lead to errors.
Our main concern was the mixing of variables containing seconds and milliseconds.
Therefore, we decided that times and durations should explicitly include their units, whereas other value types do not require any units.
### Strings
🧾 Time Immemorial
> Strings should use double quotes `"` unless the string contains them, in which case single quotes `'` can be used. For JS string templates use backticks ``` ` ``` as normal.
We needed to maintain a consistent string style across all our files.
Therefore, we selected double quotes as our standard.
## Code Style (TS / JS / Web)
### ESLint
🧾 Time Immemorial
> Use ESLint for style checking.
We needed to maintain a consistent style across all our files.
Therefore, we utilise the industry-standard ESLint, as it is familiar and highly configurable.
### Imports Within `lib` Package
🧾 Time Immemorial
> When importing files within the lib package, all names should be prefixed with "lib".
The exports of the various library files are often very generic and may conflict with variable names.
Therefore, when importing these files the "lib" prefix should be used to avoid confussing.
This only applies to files within `lib` importing other files in `lib` because all other packages import the whole of `lib` as a single package.
### Name Style (TS)
🧾 Time Immemorial
> Variables and members of classes use camelCase. Classes use PascalCase. Config values use lowercase_underscore. Exceptions allowed for over the write objects.
We needed to maintain a consistent name style across all our files.
Therefore, we follow common practive for JavaScript of using camelCase and PascalCase.
Although, we have also allowed the use of lowercase_underscore for config values to distinguish them from other variables.
## Code Style (Lua)
### FMTK
🧾 14th June 2024 | [External PR](https://github.com/justarandomgeek/vscode-factoriomod-debug/pull/126)
> Use of FMTK is not required but is advised.
> To enable Clusterio support, use the CLI flag `--clusterio-modules` under `Lua.runtime.pluginArgs`.
We needed to maintain a consistent style across all our files.
There are multiple competing standards for Lua code, and given the limited amount of Lua code in this project, we chose not to adopt any automatic linting or shared luarc file.
Nevertheless, we recomend the use of [FMTK](https://github.com/justarandomgeek/vscode-factoriomod-debug) and have taken steps towords supporting it in Clusterio.
### Name Style (Lua)
🧾 Time Immemorial
> Everything uses lowercase_underscore
We needed to maintain a consistent name style across all our files.
Therefore, we follow common practive for Lua of using lowercase_underscore.
## Code Style (Other)
### Markdown
🧾 Time Immemorial
> Use one sentence per line.
> Place two lines before each heading, except for the first one or when two headings are adjacent.
We needed to maintain a consistent code style across all our markdown files.
Therefore, we selected an approach which makes diff reports more useful and which makes headings use additional space to make them stand out.
## Repository Structure
### External Plugins
🧾 Time Immemorial
> No external plugins should be included in the main repository.
> A folder will exist which external plugin repositories can be cloned into and will be given the same treatment as first party plugins.
We require a designated space for developers to create their plugins without the concern of conflicts with first-party plugins.
To address this, we have introduced a dedicated folder for external plugins.
This solution is straightforward and has since proven to be highly effective.
### First Party Plugins
🧾 Time Immemorial
> First party plugins are included in the main repository.
> Those selected to be first party are held to the same stanards which apply to the core packages.
> Each will recieve a new release in step with the core packages.
We require a designated space for first-party plugins, separate from the core packages, to clearly represent the dependency between them.
Creating a folder provided a simple solution to this.
### Core Packages
🧾 Time Immemorial
> All core packages will be contained in the packages folder of the repository.
> They are allowed to be interdependent on each other.
We require a set of core packages to represent the different applications used in a cluster.
See [Separation of Host and Controller](#separation-of-host-and-controller).
To avoid putting more files into the repository root we created a folder for the core packages.
### TypeScript Configs
🧾 26th August 2023 | [Github PR #504](https://github.com/clusterio/clusterio/pull/504)
> Each package / plugin may contain any of: `tsconfig.json` `tsconfig.node.json` and `tsconfig.web.json`.
> The main respository root will contain all three files in addition to `tsconfig.base.json` which node and web extend from.
> The configs inside packages / plugins should extend from those in the respository root.
We found that maintaining independent tsconfig files became burdensome and required a way to consolidate them.
Initially, we introduced a base configuration file; however, this proved insufficient, so we extended it to include separate configurations for Node.js and the Web.
This approach led to each package having up to three tsconfig files.
Nevertheless, we believe this to be justified, given the need to differentiate between Node.js and Web builds while maintaining a single, shared configuration.
### Workspace
🧾 Time Immemorial
> The workspace root should contain only: configuration files, scripts used to build / clean the workspace, the readme, and the changelog.
We need to maintain a clean workspace root by only including files which effect all pacakges and plugins such as configuration files.
We have allowed for build and clean scripts to exist here as long as they are not published as part of any package or plugin.
## Project Architecture
### Deprecation of Phin in favour of `fetch`
🧾 12th December 2024 | [Discord Discussion Dec 24](https://discord.com/channels/450361298220351489/450361298220351491/1316781609206419567) | [Discord Discussion May 25](https://discord.com/channels/450361298220351489/450361298220351491/1240246997496102942)
> The builtin `fetch` method should be used as a replacement of the deprecated Phin module.
We required a replacement for the deprecated Phin HTTP client, which was used for making server-side requests and as part of testing.
With `fetch` being marked as stable in Node.js v21.0.0 and having first been made available in v16.15.0, it serves as a suitable replacement without the need to add new dependencies.
It had previously not been used because it was not marked as stable in any LTS version of NodeJS.
### Minimal Dependencies
🧾 Time Immemorial
> Any new dependencies need to be well justified.
We aim to minimise our exposure to upstream changes by using a minimal number of dependencies.
This also serves to reduce the bloat of a cluster installation and improve installation times.
Consequently, the addition of any new dependencies must be thoroughly justified, with evidence of research into alternative options and comparisons between them.
### React
🧾 Time Immemorial
> Our web stack uses ReactJS
We require a reactive website for our control panel, and ReactJS was selected due to its widespread use and familiarity.
Switching away from React for any reason would represent a significant undertaking.
### Separation of Host and Controller
🧾 Time Immemorial
> The primary application locations are Control (web and cli), Controller, and Host.
> Therefore the core packages are: `ctl` `controller` `host`. There are two libaray packages `lib` and `web_ui`.
> Additionally `create` exists to provide easy creation of a cluster.
A single application to perform all the duties of a cluster would complicate the reasoning and maintenance of the project, so we decided to split it into several packages.
The primary components of a cluster include servers connected to an instance, which runs on a host that connects to the controller, whose configuration is managed via a control connection from a CLI or web portal.
Based on this structure, we found it logical to divide the packages according to their installation location, resulting in `ctl`, `controller`, and `host`, with shared code separated into `lib` and `web_ui` depending on where it is utilised.
The additional `create` package is used by `npm create` to provide a user-friendly installation process.
### TypeScript
🧾 6th August 2023 | [Github PR #491](https://github.com/clusterio/clusterio/issues/491)
> We use TypeScript as our language of choice.
We previously used JavaScript and switched to TypeScript to provide a robust type system.
Transitioning to any other langauge would be too significant an undertaking.
Initially, we supported multiple languages for plugins, including Rust and Go; however, this approach was quickly abandoned due to its complexity.
### Webpack
🧾 Time Immemorial
> Our web stack uses Webpack and associated plugins.
We required a bundler for our web build, and at the time, Webpack was the standard.
While we have considered alternatives since, transitioning to another solution would be a significant undertaking.
## Supportability
### Breaking Changes
🧾 Time Immemorial
> We follow symver versioning for breaking changes.
> In our alpha stage, we allow every alpha to contain breaking changes as long as their are documented and provide migration actions.
> Once we enter beta, breaking changes will be more restricted.
We aim to be highly transparent with breaking changes and maintain compatibility with as many existing installations as possible.
However, as we are currently in the alpha stage, things are unstable, and breaking changes occur frequently.
As such, we have decided to document all breaking changes and provide migration steps where the changes are non-trivial.
Once we enter the beta stage, we will implement stricter restrictions on breaking changes.
### Distribution
🧾 Time Immemorial
> We use `git` and `pnpm` for development distribution.
> We use `npm` and the factorio mod portal for production distribution.
We need to distribute both production builds and development source code for the project.
By necessity, we use `npm` and the Factorio Mod Portal to distribute our production builds.
For development, we rely on `git` and GitHub, which are industry standards, while using `pnpm` as a more efficient variant of `npm` for development installations.
Although bun was considered upon its release, we were not convinced it was the right choice.
### Factorio
🧾 26th October 2024 | [Discord Discussion](https://discord.com/channels/450361298220351489/450361298220351491/1299637042350653461)
> We actively provide full support for the last two major versions of Factorio.
> This approach does not follow semantic versioning; instead, it follows the "middle" versions: 2.1 > 2.0 > 1.1 > 1.0 > 0.18 > 0.17.
> We offer passive support for all versions from 0.17 onwards, where implementation is trivial and does not impose restrictions on our actively supported versions.
Following the 2.0 update of Factorio, which introduced a large number of breaking changes, we needed to reconsider which versions would be actively supported, as it was no longer trivial to support all versions.
Anticipating that future breaking changes would also be significant, and assuming the majority of players would update to the latest stable version upon release, we decided to support the latest stable version and the previous major version that introduced breaking changes.
Support for other versions will not be removed if they have already been implemented and do not impose restrictions on our ability to support newer versions.
### General Support
🧾 Time Immemorial
> We provide full support via GitHub and our Discord server for the latest published version of Clusterio, as well as assistance with migrating from the previous published version.
> However, we do not offer any guarantees with our support; we assist when time permits and may not be able to resolve every issue.
> Additionally, while we offer support in setting up development workspaces and guidance for plugin development, we do not provide support for those running development builds as production instances.
We aim to support as many users of Clusterio as possible in setting up and using the platform for a variety of purposes, including setting up production instances, development workspaces, and providing guidance on plugin development.
However, our time to work on Clusterio is limited, and even more so our ability to offer support.
As such, we cannot assist with every issue and must set reasonable restrictions to ensure expectations are aligned.
Consequently, we do not offer any guarantees for our support, but we strive to provide as much assistance as possible for those setting up or running the latest published version.
Additionally, we support users in setting up development workspaces, as the more people willing to contribute, the better.
### NodeJS
🧾 Time Immemorial
> We support all LTS versions of NodeJS in their active and maintenance phases.
Our options were: supporting all versions, supporting all active versions, supporting all LTS versions, or supporting only the latest version.
As we aim to provide a stable, production-ready tool while still limiting the number of versions we need to manage, we chose to support all LTS versions because NodeJS can be installed by the majority of linux distro package managers which will always offer at least one LTS version.
### Operating Systems
🧾 Time Immemorial
> We support all major versions of Windows, Linux, and MacOS.
This is largely inpart to our use of NodeJS and is not a guarantee from us.
Our choice of Node.js has enabled us to support all major operating systems with minimal effort on our part.
Nevertheless, there have been instances where code paths have diverged between systems, and we have attempted to maintain support for as many systems as possible.
However, this is not a priority, and such support has often been reactive or provided by external contributors.
## Testing
### Continuous Integration
🧾 Time Immemorial
> We run unit and integration tests through GitHub CI actions as well as test coverage through CodeCov.
Continuous integration actions in GitHub are a valuable tool that we use to assist with the review of pull requests from both maintainers and contributors.
While we aim to increase the number of useful checks performed by CI in the future, this is not a priority over the development of new features.
### Continuous Deployment
🧾 Time Immemorial
> We do not use any CD actions, and have no plans of implementing them.
Unlike CI, we do not find continuous deployment useful at our current stage of the project.
This is due to the frequent breaking changes merged into the main branch, which we prefer to bundle together as a single version.
In the future, we may automate the deployment process, but it will always require manual activation.
### Mocha
🧾 Time Immemorial
> We use Mocha for testing and CodeCov for coverage.
We chose Mocha because it was a good fit when we selected it.
However, we are always looking for a better framework which is worth the effort of switching.
### Version Matrix
🧾 Time Immemorial
> We perform all tests through GitHub actions using Ubuntu latest, using all supported versions of node and all supported versions of factorio.
We perform tests on all versions of NodeJS and Factorio that we support.
However, we do not conduct tests on all supported operating systems, as the added demand on our testing resources would be significant, and the value it would provide for a cross-platform engine like NodeJS would be limited.
+67
View File
@@ -0,0 +1,67 @@
# Release workflow
## Update changelog
- CHANGELOG.md in the repository root
- The changelog.txt file in mod directories
Pull requests will usually have suggestions for changelog entries.
## Packages
> [!NOTE]
> You will need commit access to the repository as well publish access to the @clusterio org on npm to do this.
1. Ensure you are on the latest version of master branch with no untracked files
```sh
git switch master
git pull --ff-only upstream master
git status
```
2. Bump the version in the `package.json` files
```sh
sed -i 's/2\.0\.0-alpha\.XX/2\.0\.0-alpha\.YY/' {packages,plugins}/*/package.json
````
3. Tag and push the release to github
```sh
git add -p
git commit -m v2.0.0-alpha.XX
git tag v2.0.0-alpha.XX
```
4. Perform a clean install and publish the packages.
```sh
pnpm clean
pnpm -r --filter='./{plugins,packages}/*' publish
```
## Plugins
1. Bump the version in the `package.json` file
2. Run `pnpm install` in the repository root
3. Run `pnpm adduser` to authenticate
4. Run `pnpm publish` in the plugin/package directory
Before confirming the publish, ensure the packed size is approximately the same size as the last version (or within the expected size)
After publishing, test creating and installing the plugin on a fresh install.
## Lua mods
To publish lua mods like clusterio_lib, start by building from source:
```sh
pnpm run build-mod
```
or download the latest artifacts from the CI. When uploading the mods to the mod portal, make sure the latest version is the last version uploaded, as the game/mod portal/ingame updater isn't consistent in how it figures out the "latest version"
## Release notes
Release notes are expected to be published alongside new releases. We use the github releases system as well as posting in the #announcements channel on the discord. Major releases may warrant a ping.
+9 -8
View File
@@ -5,6 +5,7 @@
"scripts": {
"build": "tsc --build",
"watch": "tsc --build --watch",
"build-mod": "node ./packages/lib/build_mod.js --source-dir ./packages/host/lua/clusterio_lib",
"test": "pnpm run build && mocha --enable-source-maps --check-leaks -R spec --recursive test \"plugins/*/test/**\" --exclude \"test/manual/**\"",
"silent-test": "SILENT_TEST=y pnpm test",
"fast-test": "FAST_TEST=y pnpm test",
@@ -40,21 +41,21 @@
"@clusterio/host": "workspace:*",
"@clusterio/lib": "workspace:*",
"@clusterio/web_ui": "workspace:*",
"@swc/core": "^1.4.0",
"@swc/core": "^1.11.1",
"@typescript-eslint/eslint-plugin": "^6.4.1",
"@typescript-eslint/parser": "^6.4.1",
"eslint": "^8.45.0",
"eslint": "^8.57.1",
"eslint-plugin-node": "^11.1.0",
"express": "^4.18.2",
"form-data": "^4.0.0",
"fs-extra": "^11.1.1",
"express": "^4.21.2",
"form-data": "^4.0.2",
"fs-extra": "^11.3.0",
"jsonwebtoken": "^9.0.1",
"jszip": "^3.10.0",
"mocha": "^10.2.0",
"mocha": "^10.8.2",
"nyc": "^15.1.0",
"phin": "^3.7.0",
"typedoc": "^0.26.3",
"typescript": "^5.5.3",
"typedoc": "^0.26.11",
"typescript": "^5.7.3",
"yargs": "^17.7.2"
}
}
+14 -14
View File
@@ -3,7 +3,7 @@
"description": "Implementation of Clusterio controller",
"author": "Hornwitser",
"license": "MIT",
"version": "2.0.0-alpha.19",
"version": "2.0.0-alpha.20",
"main": "dist/node/index.js",
"scripts": {
"prepare": "tsc --build && webpack-cli --env production",
@@ -23,36 +23,36 @@
"@clusterio/lib": "workspace:*",
"@sinclair/typebox": "^0.30.4",
"busboy": "^1.6.0",
"compression": "^1.7.4",
"express": "^4.18.2",
"finalhandler": "^1.2.0",
"fs-extra": "^11.1.1",
"compression": "^1.8.0",
"express": "^4.21.2",
"finalhandler": "^1.3.1",
"fs-extra": "^11.3.0",
"jsonwebtoken": "^9.0.1",
"jszip": "^3.10.1",
"set-blocking": "^2.0.0",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^4.7.1",
"ws": "^8.13.0",
"ws": "^8.18.1",
"yargs": "^17.7.2"
},
"devDependencies": {
"@clusterio/web_ui": "workspace:*",
"@swc/core": "^1.4.0",
"@swc/core": "^1.11.1",
"@types/busboy": "^1.5.0",
"@types/compression": "^1.7.2",
"@types/express": "^4.17.17",
"@types/finalhandler": "^1.2.0",
"@types/fs-extra": "^11.0.1",
"@types/jsonwebtoken": "^9.0.2",
"@types/node": "^20.4.5",
"@types/jsonwebtoken": "^9.0.9",
"@types/node": "^20.17.19",
"@types/set-blocking": "^2.0.0",
"@types/ws": "^8.5.5",
"@types/yargs": "^17.0.24",
"@types/ws": "^8.5.14",
"@types/yargs": "^17.0.33",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.4.14",
"typescript": "^5.5.3",
"webpack": "^5.88.2",
"react-router-dom": "^6.29.0",
"typescript": "^5.7.3",
"webpack": "^5.98.0",
"webpack-cli": "^5.1.4",
"webpack-dev-middleware": "^6.1.1",
"webpack-merge": "^5.9.0"
+4
View File
@@ -127,6 +127,10 @@ export default class Controller {
lib.ModPack.fromJSON.bind(lib.ModPack)
).bootstrap());
if (modPacks.size === 0) {
modPacks.setMany(lib.ModPack.defaultModPacks);
}
const userManager = new UserManager(config);
await userManager.load(path.join(databaseDirectory, "users.json"));
+2 -2
View File
@@ -3,7 +3,7 @@
"description": "Installer for clusterio",
"author": "Hornwitser",
"license": "MIT",
"version": "2.0.0-alpha.19",
"version": "2.0.0-alpha.20",
"main": "create.js",
"bin": {
"clusteriocreate": "./create.js"
@@ -17,7 +17,7 @@
},
"dependencies": {
"chalk": "^4.1.2",
"fs-extra": "^11.1.1",
"fs-extra": "^11.3.0",
"inquirer": "^8.2.4",
"phin": "^3.7.0",
"yargs": "^17.7.2"
+5 -5
View File
@@ -3,7 +3,7 @@
"description": "Clusterio command line management tool",
"author": "Hornwitser",
"license": "MIT",
"version": "2.0.0-alpha.19",
"version": "2.0.0-alpha.20",
"main": "dist/node/index.js",
"scripts": {
"prepare": "tsc --build",
@@ -22,7 +22,7 @@
"dependencies": {
"@clusterio/lib": "workspace:*",
"as-table": "^1.0.55",
"fs-extra": "^11.1.1",
"fs-extra": "^11.3.0",
"phin": "^3.7.0",
"set-blocking": "^2.0.0",
"winston": "^3.17.0",
@@ -33,9 +33,9 @@
},
"devDependencies": {
"@types/fs-extra": "^11.0.1",
"@types/node": "^20.4.5",
"@types/node": "^20.17.19",
"@types/set-blocking": "^2.0.0",
"@types/yargs": "^17.0.24",
"typescript": "^5.5.3"
"@types/yargs": "^17.0.33",
"typescript": "^5.7.3"
}
}
@@ -1,4 +1,10 @@
---------------------------------------------------------------------------------------------------
Version: 2.0.20
Date: 25. 02. 2025
Features:
- Added support for Factorio 2.0 and space-age
- Add compatibility library with support for factorio 0.17 - 2.0
---------------------------------------------------------------------------------------------------
Version: 0.1.2
Features:
- Added support for Factorio 1.1.
+4 -4
View File
@@ -2,19 +2,19 @@
"name": "clusterio_lib",
"variants": [
{
"version": "0.1.0",
"version": "2.0.17",
"factorio_version": "0.17"
},
{
"version": "0.1.1",
"version": "2.0.18",
"factorio_version": "0.18"
},
{
"version": "0.1.2",
"version": "2.0.11",
"factorio_version": "1.1"
},
{
"version": "0.1.3",
"version": "2.0.20",
"factorio_version": "2.0"
}
],
+7 -7
View File
@@ -3,7 +3,7 @@
"description": "Implementation of Clusterio host server",
"author": "Hornwitser",
"license": "MIT",
"version": "2.0.0-alpha.19",
"version": "2.0.0-alpha.20",
"main": "dist/node/index.js",
"scripts": {
"prepare": "tsc --build",
@@ -22,13 +22,13 @@
"dependencies": {
"@clusterio/lib": "workspace:*",
"@sinclair/typebox": "^0.30.4",
"fs-extra": "^11.1.1",
"fs-extra": "^11.3.0",
"jimp": "^0.22.8",
"jszip": "^3.10.1",
"phin": "^3.7.0",
"pidusage": "^3.0.2",
"rcon-client": "^4.2.3",
"semver": "^7.5.4",
"rcon-client": "^4.2.5",
"semver": "^7.7.1",
"set-blocking": "^2.0.0",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^4.7.1",
@@ -39,11 +39,11 @@
},
"devDependencies": {
"@types/fs-extra": "^11.0.1",
"@types/node": "^20.4.5",
"@types/node": "^20.17.19",
"@types/pidusage": "^2.0.2",
"@types/semver": "^7.5.0",
"@types/set-blocking": "^2.0.0",
"@types/yargs": "^17.0.24",
"typescript": "^5.5.3"
"@types/yargs": "^17.0.33",
"typescript": "^5.7.3"
}
}
+5 -1
View File
@@ -416,7 +416,11 @@ local players = {}
for _, p in pairs(game.players) do
players[p.name] = p.online_time
end
rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
if helpers ~= nil then
rcon.print(helpers.table_to_json(players))
else
rcon.print(game.table_to_json(players))
end`.replace(/\r?\n/g, " ");
let playerTimes: Record<string, number> = JSON.parse(await this.sendRcon(exportPlayerTimes));
let count = 0;
+2
View File
@@ -348,6 +348,8 @@ const knownScenarios: Record<string, lib.ModuleInfo> = {
"4e866186ebe297f1038fd325b09df1a1f5e2fdd1": new lib.ModuleInfo("freeplay", "0.17.63", [], ["scenario"]),
// First seen in 2.0
"bcbdde18ce4ec16ebfd93bd694cd9b12ef969c9a": new lib.ModuleInfo("freeplay", "2.0.0", [], ["scenario"]),
// First seen in 2.0.29
"3af547d2f4db3728b75ac5d1b3a4f47830dc48e7": new lib.ModuleInfo("freeplay", "2.0.29", [], ["scenario"]),
};
/**
+8 -8
View File
@@ -1,7 +1,7 @@
{
"name": "@clusterio/lib",
"description": "Shared library for Clusterio",
"version": "2.0.0-alpha.19",
"version": "2.0.0-alpha.20",
"repository": "https://github.com/clusterio/clusterio",
"license": "MIT",
"scripts": {
@@ -19,17 +19,17 @@
},
"dependencies": {
"@sinclair/typebox": "^0.30.4",
"ajv": "^8.12.0",
"ajv": "^8.17.1",
"chalk": "^4.1.2",
"fast-deep-equal": "^3.1.3",
"fs-extra": "^11.1.1",
"fs-extra": "^11.3.0",
"jszip": "^3.10.1",
"klaw": "^4.1.0",
"set-blocking": "^2.0.0",
"triple-beam": "^1.4.1",
"winston": "^3.17.0",
"winston-transport": "^4.9.0",
"ws": "^8.13.0",
"ws": "^8.18.1",
"yargs": "^17.7.2"
},
"publishConfig": {
@@ -37,11 +37,11 @@
},
"devDependencies": {
"@types/fs-extra": "^11.0.1",
"@types/node": "^20.4.5",
"@types/node": "^20.17.19",
"@types/set-blocking": "^2.0.0",
"@types/triple-beam": "^1.3.2",
"@types/ws": "^8.5.5",
"@types/yargs": "^17.0.24",
"typescript": "^5.5.3"
"@types/ws": "^8.5.14",
"@types/yargs": "^17.0.33",
"typescript": "^5.7.3"
}
}
+21
View File
@@ -383,6 +383,27 @@ export default class ModPack {
}
}
/** Array of default mod packs which should exist on a newly installed cluster */
static defaultModPacks = [
...["0.17", "0.18", "1.0", "1.1", "2.0"]
.map(version => this.fromJSON({
name: `Base Game ${version}`,
description: `Factorio ${version} with no extra mods.`,
factorio_version: version,
} as any)),
...["2.0"]
.map(version => this.fromJSON({
name: `Space Age ${version}`,
description: `Factorio ${version} with Space Age expansion.`,
factorio_version: version,
mods: [
{ name: "elevated-rails", enabled: true, version },
{ name: "quality", enabled: true, version },
{ name: "space-age", enabled: true, version },
],
} as any)),
];
/**
* Returns an array of ModInfo containing all mods included in factorio
*
+43 -25
View File
@@ -9,6 +9,7 @@ import fs from "fs-extra";
import * as libFileOps from "./file_ops";
import { basicType } from "./helpers";
export type ItemCountWithQuality = { [quality: string]: number };
/**
* Converts a Map with only string keys to a JavaScript object.
@@ -154,6 +155,11 @@ function checkCount(count: unknown): asserts count is number {
}
}
function checkQuality(quality: unknown): asserts quality is string {
if (typeof quality !== "string") {
throw new Error("quality must be a string");
}
}
/**
* Item database
@@ -164,7 +170,7 @@ function checkCount(count: unknown): asserts count is number {
* deserialized the content is verified.
*/
export class ItemDatabase {
private _items: Map<string, number> = new Map();
private _items: Map<string, ItemCountWithQuality> = new Map();
/**
* Create a new item database
@@ -177,11 +183,17 @@ export class ItemDatabase {
constructor(serialized?: object) {
// Verify the content of the serialized database
if (serialized !== undefined) {
for (let [name, count] of Object.entries(serialized)) {
for (let [name, qualities] of Object.entries(serialized)) {
checkName(name);
checkCount(count);
// Migration from pre-quality database format
if (typeof qualities === "number") {
qualities = { normal: qualities };
}
for (const count of Object.values(qualities)) {
checkCount(count);
}
this._items.set(name, count);
this._items.set(name, qualities);
}
}
}
@@ -195,10 +207,15 @@ export class ItemDatabase {
* @returns Serialized representation of the database
*/
serialize() {
let obj: Record<string, number> = {};
for (let [name, count] of this._items) {
if (count !== 0) {
obj[name] = count;
let obj: Record<string, ItemCountWithQuality> = {};
for (let [name, qualities] of this._items) {
for (const quality of Object.getOwnPropertyNames(qualities)) {
if (qualities[quality] !== 0) {
if (!obj[name]) {
obj[name] = {};
}
obj[name][quality] = qualities[quality];
}
}
}
return obj;
@@ -222,16 +239,18 @@ export class ItemDatabase {
* then 0 is returned.
*
* @param name - The name of the item to get the count of.
* @param quality - The quality of the item to get the count of.
* @returns The count of the item stored.
*/
getItemCount(name: string): number {
getItemCount(name: string, quality: string): number {
checkName(name);
checkQuality(quality);
if (!this._items.has(name)) {
if (!this._items.get(name)?.[quality]) {
return 0;
}
return this._items.get(name)!;
return this._items.get(name)![quality];
}
/**
@@ -241,16 +260,21 @@ export class ItemDatabase {
*
* @param name - The name of the item to add.
* @param count - The count of item to remove.
* @param quality - The quality of the item to add.
*/
addItem(name: string, count: number) {
addItem(name: string, count: number, quality: string) {
checkName(name);
checkCount(count);
checkQuality(quality);
if (this._items.has(name)) {
count += this._items.get(name)!;
if (!this._items.has(name)) {
this._items.set(name, { [quality]: count });
return;
}
this._items.set(name, count);
// Nullish coalescing operator is used to avoid NaN when the quality is not present
let currentCount = this._items.get(name)![quality] ?? 0;
this._items.get(name)![quality] = currentCount + count;
};
/**
@@ -262,24 +286,18 @@ export class ItemDatabase {
*
* @param name - The name of the item to remove.
* @param count - The count of items to remove.
* @param quality - The quality of the item to remove.
*/
removeItem(name: string, count: number) {
checkName(name);
removeItem(name: string, count: number, quality: string) {
checkCount(count);
count = -count;
if (this._items.has(name)) {
count += this._items.get(name)!;
}
this._items.set(name, count);
this.addItem(name, -count, quality);
};
/**
* Allow to iterate through the items.
* @returns array of items.
*/
getEntries(): IterableIterator<[string, number]> {
getEntries(): IterableIterator<[string, ItemCountWithQuality]> {
return this._items.entries();
}
}
+5
View File
@@ -189,6 +189,11 @@ export abstract class Datastore<
return this.data.get(key);
}
// Allow getting the size of the datastore
get size() {
return this.data.size;
}
// Allow iterating through the datastore like a Map.
[Symbol.iterator](): IterableIterator<[K, Readonly<V>]> {
return this.data.entries();
+12 -11
View File
@@ -3,10 +3,11 @@
"description": "Clusterio web interface implementation",
"author": "Hornwitser",
"license": "MIT",
"version": "2.0.0-alpha.19",
"version": "2.0.0-alpha.19.b",
"browser": "src/index.ts",
"main": "dist/browser/src/index.js",
"scripts": {
"prepare": "tsc --build",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
@@ -17,11 +18,11 @@
"node": ">=12"
},
"dependencies": {
"@ant-design/icons": "^5.1.4",
"@ant-design/icons": "^5.6.1",
"@clusterio/lib": "workspace:*",
"@sinclair/typebox": "^0.30.4",
"@swc/core": "^1.4.0",
"antd": "^5.13.0",
"@swc/core": "^1.11.1",
"antd": "^5.24.2",
"assert": "^2.0.0",
"browserify-zlib": "^0.2.0",
"buffer": "^6.0.3",
@@ -36,14 +37,14 @@
"rc-field-form": "^1.37.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.14.2",
"react-router-dom": "^6.29.0",
"stream-browserify": "^3.0.0",
"style-loader": "^3.3.3",
"swc-loader": "^0.2.6",
"terser-webpack-plugin": "^5.3.9",
"typescript": "^5.5.3",
"terser-webpack-plugin": "^5.3.11",
"typescript": "^5.7.3",
"util": "^0.12.5",
"webpack": "^5.88.2",
"webpack": "^5.98.0",
"webpack-cli": "^5.1.4",
"webpack-manifest-plugin": "^5.0.0"
},
@@ -51,8 +52,8 @@
"access": "public"
},
"devDependencies": {
"@types/node": "^20.4.5",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7"
"@types/node": "^20.17.19",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5"
}
}
+2 -2
View File
@@ -98,8 +98,8 @@ export default function UsersPage() {
title: "Roles",
key: "roles",
render: (_, user) => (
[...user.roleIds].map(id => <Link to={`/roles/${id}/view`} onClick={e => e.stopPropagation()}>
<Tag key={id}>{(roles.get(id) || { name: id }).name}</Tag>
[...user.roleIds].map(id => <Link key={id} to={`/roles/${id}/view`} onClick={e => e.stopPropagation()}>
<Tag>{(roles.get(id) || { name: id }).name}</Tag>
</Link>)
),
},
+1 -1
View File
@@ -27,7 +27,7 @@ export function formatDuration(ms: number): string {
result += formatMinute(Math.floor(ms / 60e3));
ms %= 60e3;
}
result += formatSecond(Math.round(ms / 1e3));
result += formatSecond(Math.floor(ms / 1e3));
return result;
}
+3 -1
View File
@@ -138,7 +138,9 @@ module.exports = (env = {}) => ({
moduleIds: "deterministic",
minimizer: [
new TerserPlugin({
minify: TerserPlugin.swcMinify,
// TerserPlugin.swcMinify spams log with "false" after bump to @swc/core@1.11.1
// https://github.com/clusterio/clusterio/issues/737
// minify: TerserPlugin.swcMinify,
terserOptions: {
compress: {
keep_classnames: true,
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@clusterio/plugin-global_chat",
"version": "2.0.0-alpha.19",
"version": "2.0.0-alpha.20",
"description": "Clusterio plugin forwarding between Factorio servers",
"author": "Hornwitser <github@hornwitser.no>",
"homepage": "https://github.com/clusterio/clusterio#readme",
@@ -28,10 +28,10 @@
"devDependencies": {
"@clusterio/lib": "workspace:*",
"@clusterio/web_ui": "workspace:*",
"@types/node": "^20.4.5",
"@types/react": "^18.2.21",
"typescript": "^5.5.3",
"webpack": "^5.88.2",
"@types/node": "^20.17.19",
"@types/react": "^18.3.18",
"typescript": "^5.7.3",
"webpack": "^5.98.0",
"webpack-cli": "^5.1.4",
"webpack-merge": "^5.9.0"
},
@@ -78,6 +78,9 @@ end
function serialize.serialize_personal_logistic_slots(player)
if v2_logistic_api then
local logistic_point = player.get_requester_point()
if logistic_point == nil then
return nil
end
local serialized = {
enabled = logistic_point.enabled,
trash_not_requested = logistic_point.trash_not_requested,
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@clusterio/plugin-inventory_sync",
"version": "2.0.0-alpha.19",
"version": "2.0.0-alpha.20",
"description": "Clusterio plugin forwarding between Factorio servers",
"author": "Danielv <danielv@danielv.no>",
"homepage": "https://github.com/clusterio/factorioClusterio#readme",
@@ -33,13 +33,13 @@
"@clusterio/lib": "workspace:*",
"@clusterio/web_ui": "workspace:*",
"@types/fs-extra": "^11.0.1",
"@types/node": "^20.4.5",
"@types/react": "^18.2.21",
"antd": "^5.13.0",
"@types/node": "^20.17.19",
"@types/react": "^18.3.18",
"antd": "^5.24.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "^5.5.3",
"webpack": "^5.88.2",
"typescript": "^5.7.3",
"webpack": "^5.98.0",
"webpack-cli": "^5.1.4",
"webpack-merge": "^5.9.0"
},
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@clusterio/plugin-player_auth",
"version": "2.0.0-alpha.19",
"version": "2.0.0-alpha.20",
"description": "Clusterio plugin authenticating logged in players to the web interface",
"author": "Hornwitser <github@hornwitser.no>",
"homepage": "https://github.com/clusterio/clusterio#readme",
@@ -27,22 +27,22 @@
},
"dependencies": {
"@sinclair/typebox": "^0.30.4",
"express": "^4.16.2",
"express": "^4.21.2",
"jsonwebtoken": "^9.0.1"
},
"devDependencies": {
"@clusterio/lib": "workspace:*",
"@clusterio/web_ui": "workspace:*",
"@types/express": "^4.17.17",
"@types/jsonwebtoken": "^9.0.2",
"@types/node": "^20.4.5",
"@types/react": "^18.2.21",
"antd": "^5.13.0",
"@types/jsonwebtoken": "^9.0.9",
"@types/node": "^20.17.19",
"@types/react": "^18.3.18",
"antd": "^5.24.2",
"phin": "^3.7.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "^5.5.3",
"webpack": "^5.88.2",
"typescript": "^5.7.3",
"webpack": "^5.98.0",
"webpack-cli": "^5.1.4",
"webpack-merge": "^5.9.0"
},
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@clusterio/plugin-research_sync",
"version": "2.0.0-alpha.19",
"version": "2.0.0-alpha.20",
"description": "Clusterio plugin for synchronizing research between Factorio servers",
"author": "Hornwitser <github@hornwitser.no>",
"homepage": "https://github.com/clusterio/clusterio#readme",
@@ -33,10 +33,10 @@
"@clusterio/lib": "workspace:*",
"@clusterio/web_ui": "workspace:*",
"@types/fs-extra": "^11.0.1",
"@types/node": "^20.4.5",
"@types/react": "^18.2.21",
"typescript": "^5.5.3",
"webpack": "^5.88.2",
"@types/node": "^20.17.19",
"@types/react": "^18.3.18",
"typescript": "^5.7.3",
"webpack": "^5.98.0",
"webpack-cli": "^5.1.4",
"webpack-merge": "^5.9.0"
},
+1 -1
View File
@@ -98,7 +98,7 @@ function setForceFlowStatistic(
statisticName,
reverseDirection,
item
).set(value);
);
}
}
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@clusterio/plugin-statistics_exporter",
"version": "2.0.0-alpha.19",
"version": "2.0.0-alpha.20",
"description": "Clusterio plugin for gathering statistics from Factorio servers",
"author": "Hornwitser <github@hornwitser.no>",
"homepage": "https://github.com/clusterio/clusterio#readme",
@@ -28,10 +28,10 @@
"devDependencies": {
"@clusterio/lib": "workspace:*",
"@clusterio/web_ui": "workspace:*",
"@types/node": "^20.4.5",
"@types/react": "^18.2.21",
"typescript": "^5.5.3",
"webpack": "^5.88.2",
"@types/node": "^20.17.19",
"@types/react": "^18.3.18",
"typescript": "^5.7.3",
"webpack": "^5.98.0",
"webpack-cli": "^5.1.4",
"webpack-merge": "^5.9.0"
},
@@ -66,9 +66,19 @@ describe("statistics_exporter plugin", function() {
assert.equal(instance._instanceForceFlowStatistics.labels(
"7357", "nauvis", "player", "item_production_statistics", "input", "inserter").get(),
10);
assert.equal(instance._instanceForceFlowStatistics.labels(
"7357", "nauvis", "player", "item_production_statistics", "output", "iron-plate").get(),
24);
// Assert that it didn't record inserter consumption as production
assert.equal(instance._instanceForceFlowStatistics.labels(
"7357", "nauvis", "player", "item_production_statistics", "output", "inserter").get(),
0);
assert.equal(instance._instanceGameFlowStatistics.labels(
"7357", "nauvis", "pollution_statistics", "input", "boiler").get(),
2000);
assert.equal(instance._instanceGameFlowStatistics.labels(
"7357", "nauvis", "pollution_statistics", "output", "tree-proxy").get(),
560);
assert.equal(instance._instancePlatformMapping.labels(
"7357", "platform-1", "player", "nauvis").get(),
1);
+48 -32
View File
@@ -22,17 +22,17 @@ import {
const exportCounter = new Counter(
"clusterio_subspace_storage_export_total",
"Resources exported by instance",
{ labels: ["instance_id", "resource"] }
{ labels: ["instance_id", "resource", "quality"] }
);
const importCounter = new Counter(
"clusterio_subspace_storage_import_total",
"Resources imported by instance",
{ labels: ["instance_id", "resource"] }
{ labels: ["instance_id", "resource", "quality"] }
);
const controllerInventoryGauge = new Gauge(
"clusterio_subspace_storage_controller_inventory",
"Amount of resources stored on controller",
{ labels: ["resource"] }
{ labels: ["resource", "quality"] }
);
@@ -73,7 +73,7 @@ async function saveDatabase(
export class ControllerPlugin extends BaseControllerPlugin {
items!: lib.ItemDatabase;
itemUpdateRateLimiter!: lib.RateLimiter;
itemsLastUpdate!: Map<string, number>;
itemsLastUpdate: Map<string, lib.ItemCountWithQuality> = new Map();
subscribedControlLinks!: Set<ControlConnection>;
doleMagicId!: ReturnType<typeof setInterval>;
neuralDole!: dole.NeuralDole;
@@ -91,7 +91,10 @@ export class ControllerPlugin extends BaseControllerPlugin {
}
},
});
this.itemsLastUpdate = new Map(this.items.getEntries());
this.itemsLastUpdate = new Map();
for (const [name, qualities] of this.items.getEntries()) {
this.itemsLastUpdate.set(name, { ...qualities });
}
this.neuralDole = new dole.NeuralDole({ items: this.items });
this.doleMagicId = setInterval(() => {
@@ -116,37 +119,48 @@ export class ControllerPlugin extends BaseControllerPlugin {
}
broadcastStorage() {
let itemsToUpdateMap:Map<string, number> = new Map();
for (let [name, count] of this.items.getEntries()) {
if (this.itemsLastUpdate.get(name) === count) {
continue;
let itemsToUpdate: Item[] = [];
for (const [name, qualities] of this.items.getEntries()) {
const lastQualities = this.itemsLastUpdate.get(name);
for (const [quality, count] of Object.entries(qualities)) {
if (!lastQualities || lastQualities[quality] !== count) {
itemsToUpdate.push(new Item(name, count, quality));
}
}
itemsToUpdateMap.set(name, count);
}
if (!itemsToUpdateMap.size) {
if (!itemsToUpdate.length) {
return;
}
let itemsToUpdate = [...itemsToUpdateMap.entries()];
let update = UpdateStorageEvent.fromJSON({ items: itemsToUpdate });
let update = new UpdateStorageEvent(itemsToUpdate);
this.controller.sendTo("allInstances", update);
for (let link of this.subscribedControlLinks) {
link.send(update);
}
this.itemsLastUpdate = new Map(this.items.getEntries());
this.itemsLastUpdate = new Map();
for (const [name, qualities] of this.items.getEntries()) {
this.itemsLastUpdate.set(name, { ...qualities });
}
}
async handleGetStorageRequest() {
return [...this.items.getEntries()];
const result: Item[] = [];
for (const [name, qualities] of this.items.getEntries()) {
for (const [quality, count] of Object.entries(qualities)) {
result.push(new Item(name, count, quality));
}
}
return result;
}
async handlePlaceEvent(request: PlaceEvent, src: lib.Address) {
let instanceId = src.id;
for (let item of request.items) {
this.items.addItem(item.name, item.count);
exportCounter.labels(String(instanceId), item.name).inc(item.count);
this.items.addItem(item.name, item.count, item.quality || "normal");
exportCounter.labels(String(instanceId), item.name, item.quality || "normal").inc(item.count);
}
this.updateStorage();
@@ -164,55 +178,55 @@ export class ControllerPlugin extends BaseControllerPlugin {
let itemsRemoved = [];
if (method === "simple") {
// Give out as much items as possible until there are 0 left. This
// might lead to one host getting all the items and the rest nothing.
for (let item of request.items) {
let count = this.items.getItemCount(item.name);
const quality = item.quality || "normal";
let count = this.items.getItemCount(item.name, quality);
let toRemove = Math.min(count, item.count);
if (toRemove > 0) {
this.items.removeItem(item.name, toRemove);
itemsRemoved.push(new Item(item.name, toRemove));
this.items.removeItem(item.name, toRemove, quality);
itemsRemoved.push(new Item(item.name, toRemove, quality));
}
}
} else {
let instance = this.controller.instances.get(instanceId);
let instanceName = instance ? instance.config.get("instance.name") : "unkonwn";
let instanceName = instance ? instance.config.get("instance.name") : "unknown";
// use fancy neural net to calculate a "fair" dole division rate.
if (method === "neural_dole") {
for (let item of request.items) {
const quality = item.quality || "normal";
let count = this.neuralDole.divider(
{ name: item.name, count: item.count, instanceId, instanceName }
{ name: item.name, quality, count: item.count, instanceId, instanceName }
);
if (count > 0) {
itemsRemoved.push(new Item(item.name, count));
itemsRemoved.push(new Item(item.name, count, quality));
}
}
// Use dole division. Makes it really slow to drain out the last little bit.
} else if (method === "dole") {
for (let item of request.items) {
const quality = item.quality || "normal";
let count = dole.doleDivider({
object: { name: item.name, count: item.count, instanceId, instanceName },
object: { name: item.name, quality, count: item.count, instanceId, instanceName },
items: this.items,
logItemTransfers: this.controller.config.get("subspace_storage.log_item_transfers"),
logger: this.logger,
});
if (count > 0) {
itemsRemoved.push(new Item(item.name, count));
itemsRemoved.push(new Item(item.name, count, quality));
}
}
// Should not be possible
} else {
throw Error(`Unkown division_method ${method}`);
throw Error(`Unknown division_method ${method}`);
}
}
if (itemsRemoved.length) {
for (let item of itemsRemoved) {
importCounter.labels(String(instanceId), item.name).inc(item.count);
importCounter.labels(String(instanceId), item.name, item.quality || "normal").inc(item.count);
}
this.updateStorage();
@@ -242,8 +256,10 @@ export class ControllerPlugin extends BaseControllerPlugin {
async onMetrics() {
if (this.items) {
for (let [key, count] of this.items.getEntries()) {
controllerInventoryGauge.labels(key).set(Number(count) || 0);
for (const [name, qualities] of this.items.getEntries()) {
for (const [quality, count] of Object.entries(qualities)) {
controllerInventoryGauge.labels(name, quality).set(Number(count) || 0);
}
}
}
}
+67 -50
View File
@@ -8,23 +8,23 @@ const prometheusNNDoleGauge = new Gauge(
"clusterio_subspace_storage_nn_dole_gauge",
"Current demand being supplied by Neural Network ; 1 means all demand covered, "+
"0.5 means about half of each supply is covered, 0 means no items are given",
{ labels: ["resource"] }
{ labels: ["resource", "quality"] }
);
const prometheusDoleFactorGauge = new Gauge(
"clusterio_subspace_storage_dole_factor_gauge",
"The current dole division factor for this resource",
{ labels: ["resource"] }
{ labels: ["resource", "quality"] }
);
export class NeuralDole {
getRequestStats(itemname: string, samples: number): number {
getRequestStats(itemname: string, quality: string, samples: number): number {
let sum = 0;
samples = Math.min(samples, (this.stats[itemname] || []).length - 1);
samples = Math.min(samples, (this.stats[itemname+quality] || []).length - 1);
if (samples < 1) {
return 0.1;
}
for (let i = 1; i <= samples; i++) {
sum += this.stats[itemname][i].req;
sum += this.stats[itemname+quality][i].req;
}
if (sum === 0) {
return 0.1;
@@ -42,55 +42,67 @@ export class NeuralDole {
constructor({ items }: { items: lib.ItemDatabase }) {
// Set some storage variables for the dole divider
this.items = items;
this.itemsLastTick = new Map(items.getEntries());
this.itemsLastTick = new Map();
for (let [name, qualities] of items.getEntries()) {
for (let [quality, count] of Object.entries(qualities)) {
this.itemsLastTick.set(name+quality, count);
}
}
}
doMagic() {
for (let [name, count] of this.items.getEntries()) {
let magicData = doleNN.tick(
count,
this.dole[name],
this.itemsLastTick.get(name) || 0,
this.getRequestStats(name, 5)
);
this.stats[name] = this.stats[name] || [];
this.stats[name].unshift({ req: 0, given: 0 }); // stats[name][0] is the one we currently collect
if (this.stats[name].length>10) {
this.stats[name].pop(); // remove if too many samples in stats
}
for (let [name, qualities] of this.items.getEntries()) {
for (let [quality, count] of Object.entries(qualities)) {
let magicData = doleNN.tick(
count,
this.dole[name+quality],
this.itemsLastTick.get(name+quality) || 0,
this.getRequestStats(name, quality, 5)
);
this.stats[name+quality] = this.stats[name+quality] || [];
this.stats[name+quality].unshift({ req: 0, given: 0 }); // stats[name][0] is the one we are collecting
if (this.stats[name+quality].length>10) {
this.stats[name+quality].pop(); // remove if too many samples in stats
}
this.dole[name] = magicData[0];
// DONE handle magicData[1] for graphing for our users
prometheusNNDoleGauge.labels(name).set(magicData[1] || 0);
this.dole[name+quality] = magicData[0];
// DONE handle magicData[1] for graphing for our users
prometheusNNDoleGauge.labels(name+quality).set(magicData[1] || 0);
}
}
this.itemsLastTick = new Map();
for (let [name, qualities] of this.items.getEntries()) {
for (let [quality, count] of Object.entries(qualities)) {
this.itemsLastTick.set(name+quality, count);
}
}
this.itemsLastTick = new Map(this.items.getEntries());
}
divider(object: { name:string, count:number, instanceId:number, instanceName:string }): number {
divider(object: { name:string, quality:string, count:number, instanceId:number, instanceName:string }): number {
let magicData = doleNN.dose(
object.count, // numReq
this.items.getItemCount(object.name) || 0,
this.itemsLastTick.get(object.name) || 0,
this.dole[object.name],
this.carry[`${object.name} ${object.instanceId}`] || 0,
this.lastRequest[`${object.name}_${object.instanceId}_${object.instanceName}`] || 0,
this.getRequestStats(object.name, 5),
this.debt[`${object.name} ${object.instanceId}`] || 0
this.items.getItemCount(object.name, object.quality) || 0,
this.itemsLastTick.get(object.name+object.quality) || 0,
this.dole[object.name+object.quality],
this.carry[`${object.name} ${object.quality} ${object.instanceId}`] || 0,
this.lastRequest[`${object.name}_${object.quality}_${object.instanceId}_${object.instanceName}`] || 0,
this.getRequestStats(object.name, object.quality, 5),
this.debt[`${object.name} ${object.quality} ${object.instanceId}`] || 0
);
if ((this.stats[object.name] || []).length > 0) {
this.stats[object.name][0].req += Number(object.count);
this.stats[object.name][0].given += Number(magicData[0]);
if ((this.stats[object.name+object.quality] || []).length > 0) {
this.stats[object.name+object.quality][0].req += Number(object.count);
this.stats[object.name+object.quality][0].given += Number(magicData[0]);
}
this.lastRequest[`${object.name}_${object.instanceId}_${object.instanceName}`] = object.count;
this.lastRequest[`${object.name}_${object.quality}_${object.instanceId}_${object.instanceName}`] = object.count;
// 0. Number of items to give in that dose
// 1. New dole for item X
// 2. New carry for item X instance Y
this.dole[object.name] = magicData[1];
this.carry[`${object.name} ${object.instanceId}`] = magicData[2];
this.debt[`${object.name} ${object.instanceId}`] = magicData[3];
this.dole[object.name+object.quality] = magicData[1];
this.carry[`${object.name} ${object.quality} ${object.instanceId}`] = magicData[2];
this.debt[`${object.name} ${object.quality} ${object.instanceId}`] = magicData[3];
// Remove item from DB and send it
this.items.removeItem(object.name, magicData[0]);
this.items.removeItem(object.name, magicData[0], object.quality);
return magicData[0];
}
@@ -106,13 +118,13 @@ export function doleDivider(
logItemTransfers,
logger,
} :{
object: { name:string, count:number, instanceId:number, instanceName:string },
object: { name:string, quality:string, count:number, instanceId:number, instanceName:string },
items: lib.ItemDatabase,
logItemTransfers: boolean,
logger: lib.Logger,
},
) {
let itemCount = items.getItemCount(object.name);
let itemCount = items.getItemCount(object.name, object.quality);
// lower rates will equal more dramatic swings
const doleDivisionRetardation = 10;
// a higher cap will divide the store more ways, but will take longer to
@@ -120,13 +132,14 @@ export function doleDivider(
const maxDoleDivision = 250;
const originalCount = Number(object.count) || 0;
object.count /= ((_doleDivisionFactor[object.name] || 0) + doleDivisionRetardation) / doleDivisionRetardation;
const doleFactor = _doleDivisionFactor[object.name+object.quality] || 0;
object.count /= (doleFactor + doleDivisionRetardation) / doleDivisionRetardation;
object.count = Math.round(object.count);
if (logItemTransfers) {
logger.verbose(
`Serving ${object.count}/${originalCount} ${object.name} from ${itemCount} ${object.name} `+
`with dole division factor ${(_doleDivisionFactor[object.name]||0)} `+
`(real=${((_doleDivisionFactor[object.name] || 0) + doleDivisionRetardation) / doleDivisionRetardation}), `+
`with dole division factor ${doleFactor} `+
`(real=${doleFactor + doleDivisionRetardation}), `+
`item is ${itemCount > object.count?"stocked":"short"}.`
);
}
@@ -134,18 +147,22 @@ export function doleDivider(
// Update existing items if item name already exists
if (itemCount > object.count) {
// If successful, increase dole
_doleDivisionFactor[object.name] = Math.max(_doleDivisionFactor[object.name] || 1, 1) - 1;
items.removeItem(object.name, object.count);
const key = object.name + object.quality;
_doleDivisionFactor[key] = Math.max(_doleDivisionFactor[key] || 1, 1) - 1;
items.removeItem(object.name, object.count, object.quality);
prometheusDoleFactorGauge.labels(object.name).set(_doleDivisionFactor[object.name] || 0);
prometheusDoleFactorGauge
.labels(object.name, object.quality)
.set(_doleDivisionFactor[key] || 0);
return object.count;
}
// if we didn't have enough, attempt giving out a smaller amount next time
_doleDivisionFactor[object.name] = Math.min(
maxDoleDivision, Math.max(_doleDivisionFactor[object.name] || 1, 1) * 2
);
prometheusDoleFactorGauge.labels(object.name).set(_doleDivisionFactor[object.name] || 0);
const key = object.name + object.quality;
_doleDivisionFactor[key] = Math.min(maxDoleDivision, Math.max(_doleDivisionFactor[key] || 1, 1) * 2);
prometheusDoleFactorGauge
.labels(object.name, object.quality)
.set(_doleDivisionFactor[key] || 0);
return 0;
// console.log(`failure out of ${object.name}/${object.count} from ${object.instanceID} (${object.instanceName})`);
}
+7 -5
View File
@@ -9,7 +9,7 @@ import {
Item,
} from "./messages";
type IpcItems = [string, number][];
type IpcItems = [name: string, count: number, quality: string][];
export class InstancePlugin extends BaseInstancePlugin {
pendingTasks!: Set<any>;
@@ -26,6 +26,8 @@ export class InstancePlugin extends BaseInstancePlugin {
this.pendingTasks = new Set();
this.instance.server.on("ipc-subspace_storage:output", (output: IpcItems) => {
this.logger.info("Received output items:");
this.logger.info(JSON.stringify(output));
this.provideItems(output).catch(err => this.unexpectedError(err));
});
this.instance.server.on("ipc-subspace_storage:orders", (orders: IpcItems) => {
@@ -78,8 +80,7 @@ export class InstancePlugin extends BaseInstancePlugin {
return;
}
const fromIpcItems = items.map(item => new Item(item[0], item[1]));
const fromIpcItems = items.map(item => new Item(item[0], item[1], item[2]));
this.instance.sendTo("controller", new PlaceEvent(fromIpcItems));
if (this.instance.config.get("subspace_storage.log_item_transfers")) {
@@ -90,8 +91,9 @@ export class InstancePlugin extends BaseInstancePlugin {
// request items --------------------------------------------------------------
async requestItems(requestItems: IpcItems) {
this.logger.info(`Requesting items: ${JSON.stringify(requestItems)}`);
// Request the items all at once
let fromIpcItems = requestItems.map(item => new Item(item[0], item[1]));
const fromIpcItems = requestItems.map(item => new Item(item[0], item[1], item[2]));
let items = await this.instance.sendTo("controller", new RemoveRequest(fromIpcItems));
if (!items.length) {
@@ -115,7 +117,7 @@ export class InstancePlugin extends BaseInstancePlugin {
let items = event.items;
// XXX this should be moved to instance/clusterio api
items.push(new Item("signal-unixtime", Math.floor(Date.now()/1000)));
items.push(new Item("signal-unixtime", Math.floor(Date.now()/1000), "normal"));
let itemsJson = lib.escapeString(JSON.stringify(items));
let task = this.sendRcon(`/sc __subspace_storage__ UpdateInvData("${itemsJson}")`, true);
+5 -4
View File
@@ -1,25 +1,26 @@
import { Type, Static } from "@sinclair/typebox";
import * as lib from "@clusterio/lib";
import { type } from "os";
export class Item {
constructor(
public name: string,
public count: number
public count: number,
public quality: string = "normal"
) {
}
static jsonSchema = Type.Tuple([
Type.String(),
Type.Number(),
Type.String(),
]);
toJSON() {
return [this.name, this.count];
return [this.name, this.count, this.quality];
}
static fromJSON(json: Static<typeof Item.jsonSchema>): Item {
return new this(json[0], json[1]);
return new this(json[0], json[1], json[2]);
}
}
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@clusterio/plugin-subspace_storage",
"version": "2.0.0-alpha.18",
"version": "2.0.0-alpha.20",
"description": "Clusterio plugin for sharing storage between Factorio servers",
"author": "Hornwitser <github@hornwitser.no>",
"homepage": "https://github.com/clusterio/clusterio#readme",
@@ -34,13 +34,13 @@
"@clusterio/web_ui": "workspace:*",
"@types/express": "^4.17.17",
"@types/fs-extra": "^11.0.1",
"@types/node": "^20.4.5",
"@types/react": "^18.2.21",
"antd": "^5.13.0",
"@types/node": "^20.17.19",
"@types/react": "^18.3.18",
"antd": "^5.24.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "^5.5.3",
"webpack": "^5.88.2",
"typescript": "^5.7.3",
"webpack": "^5.98.0",
"webpack-cli": "^5.1.4",
"webpack-merge": "^5.9.0"
},
+18 -13
View File
@@ -35,7 +35,7 @@ function StoragePage() {
let locale = useLocale();
let itemMetadata = useItemMetadata();
let storage = useStorage(control);
type ItemFilter = ([name, count]: [string, number]) => boolean;
type ItemFilter = ([name, item]: [string, Item]) => boolean;
let [filter, setFilter] = useState<null | ItemFilter>(null);
function getLocaleName(itemName: string) {
@@ -73,9 +73,9 @@ function StoragePage() {
search = search.replace(/(^| )(\w)/g, "$1\\b$2");
search = search.replace(/ +/g, ".*");
let filterExpr = new RegExp(search, "i");
setFilter(() => ((item: [string, number]) => {
let name = getLocaleName(item[0]);
return filterExpr.test(name) || filterExpr.test(item[0]);
setFilter(() => ((item: [string, Item]) => {
let name = getLocaleName(item[1].name);
return filterExpr.test(name) || filterExpr.test(item[1].name);
}));
}}
/>
@@ -86,29 +86,34 @@ function StoragePage() {
title: "Resource",
key: "resource",
sorter: (a, b) => {
let aName = getLocaleName(a[0]);
let bName = getLocaleName(b[0]);
let aName = getLocaleName(a[1].name);
let bName = getLocaleName(b[1].name);
if (aName < bName) { return -1; }
if (aName > bName) { return 1; }
return 0;
},
render: (_, item) => {
let localeName = getLocaleName(item[0]);
let hasMeta = itemMetadata.get(item[0]);
let localeName = getLocaleName(item[1].name);
let hasMeta = itemMetadata.get(item[1].name);
return <>
<span className={`factorio-icon item-${hasMeta ? item[0] : "unknown-item"}`}/>
<span className={`factorio-icon item-${hasMeta ? item[1].name : "unknown-item"}`}/>
{localeName}
</>;
},
},
{
title: "Quality",
key: "quality",
render: (_, item) => item[1].quality,
},
{
title: "Quantity",
key: "quantity",
align: "right",
defaultSortOrder: "descend",
sorter: (a, b) => a[1] - b[1],
render: (_, item) => numberFormat.format(item[1]),
sorter: (a, b) => a[1].count - b[1].count,
render: (_, item) => numberFormat.format(item[1].count),
},
]}
dataSource={filter ? storage.filter(filter) : storage}
@@ -119,7 +124,7 @@ function StoragePage() {
}
export class WebPlugin extends BaseWebPlugin {
storage = new Map<string, number>();
storage = new Map<string, Item>();
callbacks: (() => void)[] = [];
async init() {
@@ -165,7 +170,7 @@ export class WebPlugin extends BaseWebPlugin {
updateStorage(items: Item[]) {
for (let item of items) {
this.storage.set(item.name, item.count);
this.storage.set(`${item.name}:${item.quality}`, item);
}
for (let callback of this.callbacks) {
callback();
+15 -4
View File
@@ -199,12 +199,16 @@ describe("Integration of Clusterio", function() {
it("should download mods from controller", async function() {
slowTest(this);
await runWithAltHost(async () => {
const clusterioLib = path.join("temp", "test", "alt-mods", "clusterio_lib_0.1.2.zip");
assert(!await fs.pathExists(clusterioLib), "mod was downloaded before the test");
// Use the latest vesion in /dist instead of a hardcoded version
async function checkModDownloaded() {
let files = await fs.readdir(path.join("temp", "test", "alt-mods"));
return files.find(name => name.startsWith("clusterio_lib_"));
}
assert(!await checkModDownloaded(), "mod was downloaded before the test");
await execCtl("instance create alt-mod --id 98");
await execCtl("instance assign alt-mod 5");
await execCtl("instance save create alt-mod");
assert(await fs.pathExists(clusterioLib), "mod was not downloaded by the test");
assert(await checkModDownloaded(), "mod was not downloaded by the test");
});
});
it("should auto start instances with auto_start enabled", async function() {
@@ -1072,8 +1076,15 @@ describe("Integration of Clusterio", function() {
});
describe("mod-pack list", function() {
let result = null;
it("runs", async function() {
await execCtl("mod-pack list");
result = await execCtl("mod-pack list");
});
it("contains defaults", function() {
assert(result !== null, "Failed to return a value");
const stdout = result.stdout.trim();
assert(stdout.indexOf("Base Game") >= 0, "No base game pack");
assert(stdout.indexOf("Space Age") >= 0, "No space age pack");
});
});
+2 -24
View File
@@ -18,9 +18,6 @@ require("../../plugins/research_sync/dist/node/index");
require("../../plugins/statistics_exporter/dist/node/index");
require("../../plugins/subspace_storage/dist/node/index");
const subspaceStorageVersion = "1.99.8";
const subspaceStorageGitHubTag = "1.99.6"; // Tag subspaceStorageVersion was released under on GitHub
class TestControl extends lib.Link {
constructor(connector, subscribe = true) {
super(connector);
@@ -266,24 +263,6 @@ before(async function() {
sourceDir: "packages/host/lua/clusterio_lib",
outputDir: modsDir,
});
const ssv = subspaceStorageVersion;
const ssTag = subspaceStorageGitHubTag;
const ssZip = `subspace_storage_${ssv}.zip`;
if (!await fs.pathExists(`mods/${ssZip}`)) {
console.log(`Downloading ${ssZip}`);
const ssUrl = `https://github.com/clusterio/subspace_storage/releases/download/${ssTag}/${ssZip}`;
let response = await phin({
url: ssUrl,
method: "GET",
stream: false, // phin leaks open sockets when stream and followRedirects is combined
followRedirects: true,
});
if (response.statusCode !== 200) {
throw new Error(`${ssUrl} returned ${response.statusCode} ${response.statusMessage}`);
}
await fs.outputFile(`mods/${ssZip}`, response.body);
}
await fs.copyFile(`mods/${ssZip}`, `${modsDir}/${ssZip}`);
console.log("Setting Controller Config");
await exec("node ../../packages/controller config set controller.auth_secret TestSecretDoNotUse");
@@ -325,9 +304,8 @@ before(async function() {
const testPack = lib.ModPack.fromJSON({});
testPack.id = 12;
testPack.name = "subspace_storage-pack";
testPack.factorioVersion = "1.1.0";
testPack.mods.set("clusterio_lib", { name: "clusterio_lib", enabled: true, version: "0.1.2" });
testPack.mods.set("subspace_storage", { name: "subspace_storage", enabled: true, version: subspaceStorageVersion });
testPack.factorioVersion = "2.0.0";
testPack.mods.set("clusterio_lib", { name: "clusterio_lib", enabled: true, version: "2.0.20" });
await control.sendTo("controller", new lib.ModPackCreateRequest(testPack));
await control.sendTo(
"controller",
+20 -4
View File
@@ -27,8 +27,7 @@ describe("lib/data/ModPack", function() {
check(ModPack.fromJSON({ description: "My Description" }));
check(ModPack.fromJSON({ factorio_version: "2.0" }));
check(ModPack.fromJSON({ mods: [
{ name: "subspace_storage", enabled: true, version: "1.99.8" },
{ name: "clusterio_lib", enabled: true, version: "0.1.2", sha1: "012345abcd" },
{ name: "clusterio_lib", enabled: true, version: "2.0.20", sha1: "012345abcd" },
]}));
check(ModPack.fromJSON({ settings: {
"startup": {
@@ -49,8 +48,7 @@ describe("lib/data/ModPack", function() {
description: "Every option at once.",
factorio_version: "2.0",
mods: [
{ name: "subspace_storage", enabled: true, version: "1.99.8" },
{ name: "clusterio_lib", enabled: true, version: "0.1.2", sha1: "012345abcd" },
{ name: "clusterio_lib", enabled: true, version: "2.0.20", sha1: "012345abcd" },
],
settings: {
"startup": {
@@ -378,6 +376,24 @@ describe("lib/data/ModPack", function() {
/* eslint-enable indent */
});
});
describe("defaultModPacks", function() {
it("should contain at least one base game only", function() {
const modPack = ModPack.defaultModPacks.find(pack => {
const base = pack.mods.get("base");
const spaceAge = pack.mods.get("space-age");
return base && base.enabled && (!spaceAge || !spaceAge.enabled);
});
assert(modPack, "Does not contain base game only");
});
it("should contain at least one space age expansion", function() {
const modPack = ModPack.defaultModPacks.find(pack => {
const base = pack.mods.get("base");
const spaceAge = pack.mods.get("space-age");
return base && base.enabled && spaceAge && spaceAge.enabled;
});
assert(modPack, "Does not contain space age expansion");
});
});
describe("getBuiltinMods()", function() {
it("should work with versions before 2.0", function() {
const builtinMods = ModPack.getBuiltinMods("1.1");
+19 -19
View File
@@ -169,7 +169,7 @@ describe("lib/database", function() {
it("should restore the passed serialized database", function() {
let items = new lib.ItemDatabase({ "a": 1, "b": 2 });
assert.deepEqual(items._items, new Map([["a", 1], ["b", 2]]));
assert.deepEqual(items._items, new Map([["a", { normal: 1 }], ["b", { normal: 2 }]]));
});
it("should throw on invalid serialized database", function() {
@@ -188,7 +188,7 @@ describe("lib/database", function() {
describe(".serialize()", function() {
it("should return a serialized database", function() {
let items = new lib.ItemDatabase({ "a": 10 });
assert.deepEqual(items.serialize(), { "a": 10 });
assert.deepEqual(items.serialize(), { "a": { normal: 10 } });
});
it("should remove zero count entries", function() {
@@ -207,18 +207,18 @@ describe("lib/database", function() {
describe(".getItemCount()", function() {
it("should return the count of the given item", function() {
let items = new lib.ItemDatabase({ "a": 10 });
assert.equal(items.getItemCount("a"), 10);
assert.equal(items.getItemCount("a", "normal"), 10);
});
it("should return zero if item does not exist", function() {
let items = new lib.ItemDatabase();
assert.equal(items.getItemCount("b"), 0);
assert.equal(items.getItemCount("b", "normal"), 0);
});
it("should throw on invalid name", function() {
let items = new lib.ItemDatabase();
assert.throws(
() => items.getItemCount(2),
() => items.getItemCount(2, "normal"),
new Error("name must be a string")
);
});
@@ -227,20 +227,20 @@ describe("lib/database", function() {
describe(".addItem()", function() {
it("should add a new item", function() {
let items = new lib.ItemDatabase();
items.addItem("a", 10);
assert.deepEqual(items._items, new Map([["a", 10]]));
items.addItem("a", 10, "normal");
assert.deepEqual(items._items, new Map([["a", { normal: 10 }]]));
});
it("should add an existing item", function() {
let items = new lib.ItemDatabase({ "a": 10 });
items.addItem("a", 10);
assert.deepEqual(items._items, new Map([["a", 20]]));
items.addItem("a", 10, "normal");
assert.deepEqual(items._items, new Map([["a", { normal: 20 }]]));
});
it("should throw on invalid name", function() {
let items = new lib.ItemDatabase();
assert.throws(
() => items.addItem(2, 10),
() => items.addItem(2, 10, "normal"),
new Error("name must be a string")
);
});
@@ -248,11 +248,11 @@ describe("lib/database", function() {
it("should throw on invalid count", function() {
let items = new lib.ItemDatabase();
assert.throws(
() => items.addItem("a", NaN),
() => items.addItem("a", NaN, "normal"),
new Error("count must be a number")
);
assert.throws(
() => items.addItem("a", "1"),
() => items.addItem("a", "1", "normal"),
new Error("count must be a number")
);
});
@@ -261,20 +261,20 @@ describe("lib/database", function() {
describe(".removeItem()", function() {
it("should remove an existing item", function() {
let items = new lib.ItemDatabase({ "a": 20 });
items.removeItem("a", 10);
assert.deepEqual(items._items, new Map([["a", 10]]));
items.removeItem("a", 10, "normal");
assert.deepEqual(items._items, new Map([["a", { normal: 10 }]]));
});
it("should turn a non-existing item negative", function() {
let items = new lib.ItemDatabase();
items.removeItem("a", 10);
assert.deepEqual(items._items, new Map([["a", -10]]));
items.removeItem("a", 10, "normal");
assert.deepEqual(items._items, new Map([["a", { normal: -10 }]]));
});
it("should throw on invalid name", function() {
let items = new lib.ItemDatabase();
assert.throws(
() => items.removeItem(2, 10),
() => items.removeItem(2, 10, "normal"),
new Error("name must be a string")
);
});
@@ -282,11 +282,11 @@ describe("lib/database", function() {
it("should throw on invalid count", function() {
let items = new lib.ItemDatabase();
assert.throws(
() => items.removeItem("a", "b"),
() => items.removeItem("a", "b", "normal"),
new Error("count must be a number")
);
assert.throws(
() => items.removeItem("a", "1"),
() => items.removeItem("a", "1", "normal"),
new Error("count must be a number")
);
});
+16
View File
@@ -413,6 +413,22 @@ describe("lib/datastore", function() {
});
});
describe("size", function() {
it("returns the correct size", function() {
assert.equal(datastore.size, 3);
});
});
describe("iterator", function() {
it("iterates all values", function() {
const entries = [];
for (const entry of datastore) {
entries.push(entry);
}
assert.deepEqual(entries, [...datastoreProvider.value.entries()]);
});
});
describe("values", function() {
it("returns all values", function() {
assert.deepEqual([...datastore.values()], [...datastoreProvider.value.values()]);