mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 15:04:52 +09:00
Merge branch 'master' into feature/plugin-template
This commit is contained in:
147 files changed
+1587
-570
No files matched your search
+3
-2
@@ -35,6 +35,7 @@ module.exports = {
|
||||
"rules": {
|
||||
"prefer-arrow-callback": "off",
|
||||
"no-invalid-this": "off",
|
||||
"valid-jsdoc": "off",
|
||||
"node/no-unpublished-import": "off",
|
||||
"node/no-unpublished-require": "off",
|
||||
},
|
||||
@@ -333,7 +334,7 @@ module.exports = {
|
||||
"semi-style": "off",
|
||||
"sort-imports": "off",
|
||||
"sort-keys": "off",
|
||||
"sort-vars": "error",
|
||||
"sort-vars": "off",
|
||||
"space-before-blocks": "error",
|
||||
"space-before-function-paren": "off",
|
||||
"space-in-parens": ["error", "never"],
|
||||
@@ -404,7 +405,7 @@ module.exports = {
|
||||
"node/process-exit-as-throw": "error",
|
||||
"node/shebang": ["error", {
|
||||
"convertPath": {
|
||||
"*.ts": ["^(.+)\\.ts$", "dist/$1.js"],
|
||||
"*.ts": ["^(.+)\\.ts$", "dist/node/$1.js"],
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
+83
-1
@@ -1,10 +1,86 @@
|
||||
# Changelog
|
||||
|
||||
<!-- Template for entries. Add sections that apply as changes are made.
|
||||
## Version 2.0.0-alpha.xx
|
||||
|
||||
### Major Features
|
||||
|
||||
- Major additions and/or major improvements to managing clusters.
|
||||
|
||||
### Features
|
||||
|
||||
- New features added.
|
||||
|
||||
### Changes
|
||||
|
||||
- Bugfixes and small changes to existing behaviour.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Changes which break backwards compatibility with older hosts or plugins code.
|
||||
|
||||
Many thanks to the following for contributing to this release:
|
||||
[@username](https://github.com/username)
|
||||
-->
|
||||
|
||||
## Version 2.0.0-alpha.18
|
||||
|
||||
### Features
|
||||
|
||||
- Added logging of HTTP requests and errors on the controller [#556](https://github.com/clusterio/clusterio/pull/556).
|
||||
|
||||
### Changes
|
||||
|
||||
- Fixed Inventory Sync reporting a negative database size [¤598](https://github.com/clusterio/clusterio/pull/598).
|
||||
|
||||
Many thanks to the following for contributing to this release:
|
||||
[@Cooldude2606](https://github.com/Cooldude2606)
|
||||
|
||||
|
||||
## Version 2.0.0-alpha.17
|
||||
|
||||
### Features
|
||||
|
||||
- Made the host an instance is assigned to link to the host page for that host in the Web UI [#587](https://github.com/clusterio/clusterio/pull/587).
|
||||
- Added pagination to the saves list.
|
||||
|
||||
### Changes
|
||||
|
||||
- Fixed an unhandled error in Subspace Storage crashing the host.
|
||||
- Fixed an unhandled rejection in disconnect logic crashing the host.
|
||||
- Fixed instance screen in Web UI not updating when an instance was reassigned.
|
||||
- Fixed an error thrown in the rendering of one page in the Web UI causing all pages to become inaccessible.
|
||||
- Fixed Web UI not showing instance saves when the permission to list and subscribe the saves is granted but the user is not a cluster admin.
|
||||
- Similarly fixed Web UI not showing the mod pack create, delete, list and host revoke token actions if the user is not a cluster admin.
|
||||
- Fixed Research Sync applying progress of a previous techology level to the level on the controller when it's the active research during instance startup.
|
||||
- Fixed data export with a mod using shorthand notation for the default value of a color-setting causing the ModPack data to be set to invalid data.
|
||||
- Fixed per user per instance player stats not being deleted when the instance it's for is deleted.
|
||||
- Fixed mods setting the item prototype icons property to an object mapping instead of an array breaking the data export.
|
||||
- Fixed a bad mod in the host's mods folder causing instance start to fail instead of the host re-downloading the mod.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Renamed the permission `core.instance.save.list.subscribe` to `core.instance.save.subscribe`.
|
||||
|
||||
Many thanks to the following for contributing to this release:
|
||||
[@Cooldude2606](https://github.com/Cooldude2606)
|
||||
[@Danielv123](https://github.com/Danielv123)
|
||||
[@Hornwitser](https://github.com/Hornwitser)
|
||||
|
||||
|
||||
## Version 2.0.0-alpha.16
|
||||
|
||||
### Changes
|
||||
|
||||
- Fixed message sequences not being renumbered when forwarded between links causing reconnects to send duplicated messages.
|
||||
|
||||
|
||||
## Version 2.0.0-alpha.15
|
||||
|
||||
### Features
|
||||
|
||||
- Added support for `color-settings` Mod settings (#568)[https://github.com/clusterio/clusterio/issues/568].
|
||||
- Added support for `color-settings` Mod settings [#568](https://github.com/clusterio/clusterio/issues/568).
|
||||
- Added `clusterio_controller_pending_requests` and `clusterio_host_pending_requests` metrics.
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -12,6 +88,12 @@
|
||||
- Fixed ctl not exporting types used for adding commands to ctl plugins.
|
||||
- Fixed controller only plugins throwing an error about missing a config field on startup out when added to hosts.
|
||||
- Fixed installer incorrectly claiming the options --factorio-dir and --no-download-headless conflicts with each other.
|
||||
- Fixed a memory leak on every request sent over the WebSocket.
|
||||
- Fixed assertion error on closing or invalidating a link with messages forwarded from a virtual link.
|
||||
- Fixed host hanging if stopped while an instance is in the process of creating a save during startup.
|
||||
- Fixed `instance.auto_start` option.
|
||||
- Fixed host log spammed with errors when plugins broadcast messages to instances with the plugin disabled.
|
||||
- Fixed properties of object config fields not being possible to remove.
|
||||
|
||||
|
||||
## Version 2.0.0-alpha.14
|
||||
|
||||
@@ -268,9 +268,12 @@ If you want to use a different user for the command line interface, you can gene
|
||||
|
||||
## Known Issues
|
||||
|
||||
- The `instance.auto_start` option is currently is broken and will not work, turn it off and do not use it for now.
|
||||
- When the `server_select` plugin is installed you may see `[error] Unhandled event server_select:UpdateInstancesEvent` logged when starting an instance.
|
||||
This should be ignored as it does not indicate an error occured.
|
||||
- After updating Factorio you may get the error `Error: Expected empty response but got "Cannot execute command. Error: [string "clusterio_private.update_instance(..."]:1: attempt to index global 'clusterio_private' (a nil value)?` on instance start.
|
||||
This is due to Factorio applying a migration replacing the patched content of the save with the original scenario, attempting to start the instance again should resolve the issue.
|
||||
- In the mod pack view of the web interface the "base" mod is displayed with an error complaining about the mod being missing from the controller storage.
|
||||
This is due to the "base" mod currently not being connectly handled as a built-in mod to Factorio in the web interface, this error message should be ignored.
|
||||
- After migrating from Alpha 13 to Alpha 14 running an instance data export fails with the error `Upload failed: 401 Unauthorized` due to the host token being issued to a slave.
|
||||
To workaround this find the id of the host in the host config and generate a new host token with the same id.
|
||||
Stop the host and replace the value of the `host.controller_token` field in `config-host.json` with the new host token before starting it up again.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
if_ci_failed: success
|
||||
@@ -81,6 +81,9 @@ Note that to use `clusterioctl` you will have to create a control config first:
|
||||
|
||||
Once you've set up the cluster you can use `node packages/controller run` to run the controller and `node packages/host run` to run the host which connects to the controller.
|
||||
|
||||
When editing TypeScript files ran by Node.js you need to run `pnpm build` to compile the files to JavaScript before restarting the controller/host/ctl for changes take effect.
|
||||
To have TypeScript watch for changes and rebuild immediadly you may run `pnpm watch` instead.
|
||||
|
||||
For web development on the controller there are also the following flags:
|
||||
|
||||
`--dev` - Recompile the `web_ui` package on the fly providing live reloading of it.
|
||||
@@ -316,6 +319,6 @@ You will need commit access to the repository as well publish access to the @clu
|
||||
4. Publish the packages.
|
||||
|
||||
```sh
|
||||
# Warning: Double check your current working directory is the repository before running this
|
||||
pnpm -r --filter='!./external_plugins/**' publish
|
||||
rm -r ./{plugins,packages}/*/dist
|
||||
pnpm -r --filter='./{plugins,packages}/*' publish
|
||||
```
|
||||
@@ -78,7 +78,7 @@ import { BaseInstancePlugin } from "@clusterio/host";
|
||||
|
||||
Previously the info of a plugin would be loaded by requiring `./plugin_dir/info`, this has been changed to require `./plugin_dir` which in turn means that `info.js` is no longer the default entrypoint.
|
||||
To solve this you can either add `"main": "info.js",` to package.json or rename `info.js` to `index.js` (the rest of this guide assumes the entrypoint was renamed to `index.js`).
|
||||
The motivation behind this change was to allow TypeScript to output its build to a sub-folder of the plugin, if you're using TypeScript you'll likely have the entrypoint be located in `dist/index.js` instead.
|
||||
The motivation behind this change was to allow TypeScript to output its build to a sub-folder of the plugin, if you're using TypeScript you'll likely have the entrypoint be located in `dist/node/index.js` instead.
|
||||
|
||||
The expected place where the declaration of a plugin is exported has also changed from `module.exports` to `module.exports.plugin`:
|
||||
|
||||
|
||||
+98
-138
@@ -12,8 +12,6 @@ The plugin classes have pre-defined hooks that are called during various stages
|
||||
- [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)
|
||||
@@ -326,175 +324,137 @@ Events are simple one-way notifications that invoke a handler on the target it's
|
||||
Requests are pairs of request and response messages where the request is sent to the target and the response is the reply back from the target.
|
||||
The requests are similar to HTTP requests, except both parties of a link may initiate one.
|
||||
|
||||
|
||||
### Defining Events
|
||||
|
||||
Events are defined as properties of the messages object exported on `plugin` 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 `plugin`:
|
||||
|
||||
```js
|
||||
messages: {
|
||||
startFrobnication: new lib.Event({
|
||||
type: "foo_frobber:start_frobnication",
|
||||
links: ["controller-host", "host-instance"],
|
||||
forwardTo: "instance",
|
||||
eventRequired: ["frobnication_type"],
|
||||
eventProperties: {
|
||||
"frobnication_type": { type: "string" },
|
||||
"urgent": { type: "boolean" },
|
||||
},
|
||||
}),
|
||||
},
|
||||
```
|
||||
|
||||
This specifies an event that can be sent from the controller to a host, and from a host to an instance.
|
||||
It also specifies that the event must contain the property `frobnication_type`, with a string value in the data payload and that it may optionally contain a boolean `urgent` property.
|
||||
It will also be forwarded by hosts 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, `"controller-host"` means the event can be sent from the controller to the host, but can't be sent back the other way, unless `"host-controller"` is also present in the links array.
|
||||
|
||||
The available endpoints are `controller`, `host`, `instance`, and `control`.
|
||||
Controller talks with host and control, and host 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 controller to instance it must have both `"controller-host"` and `"host-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 `"controller"`, to indicate a host should forward it to the controller, 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 the event to a host from an instance will cause it to be broadcast to all instances of that host except for the instance it came from.
|
||||
|
||||
#### eventRequired
|
||||
|
||||
By default all properties defined in `eventProperties` are required to be present in the payload for the event.
|
||||
This can be overriden by specifying an array of properties which are required here.
|
||||
This is equivalent to using the `required` keyword in JSON schema.
|
||||
|
||||
#### 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 by default 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 `"controller"` 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-host`, `host-controller`, `controller-host`, and `host-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 `plugin` 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.
|
||||
Messages are defined as items of the `plugin.messages` array exported by `index.js`.
|
||||
For example, the following could be defined in `plugin`:
|
||||
|
||||
```js
|
||||
messages: {
|
||||
reportFrobnication: new lib.Request({
|
||||
type: "foo_frobber:report_frobnication",
|
||||
links: ["controller-host", "host-instance"],
|
||||
forwardTo: "instance",
|
||||
requestRequired: ["verbosity"],
|
||||
requestProperties: {
|
||||
"verbosity": { type: "integer" },
|
||||
"special": { type: "boolean" },
|
||||
},
|
||||
responseProperties: {
|
||||
"report": {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
messages: [
|
||||
class Frobnicate {
|
||||
static plugin = "foo_frobber"; // Plugin name
|
||||
static type = "request";
|
||||
static src = "controller";
|
||||
static dst = "instance";
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
"verbosity": { type: "integer" },
|
||||
"special": { type: "boolean" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
required: ["verbosity"],
|
||||
additionalProperties: false,
|
||||
};
|
||||
constructor(json) {
|
||||
this.verbosity = json.verbosity;
|
||||
this.special = json.special;
|
||||
}
|
||||
static fromJSON(json) {
|
||||
return new Frobnicate(json);
|
||||
}
|
||||
toJSON() {
|
||||
return {
|
||||
verbosity: this.verbosity,
|
||||
special: this.special,
|
||||
};
|
||||
}
|
||||
static Response = {
|
||||
jsonSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
"report": {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
fromJSON(json) {
|
||||
return json
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
This specifies a request that can be sent from the controller to a host, and from a host 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 it may also contain a boolean `special` property.
|
||||
It also defines that the response sent must contain a `report` property mapping to an array of strings.
|
||||
When received by a host, it will also be forwarded to the instance specified by `instance_id`.
|
||||
This specifies a request that can be sent from the controller to an instance, being routed through the correct host behind the scenes.
|
||||
The request data must contain the property `verbosity` with an integer number as the value, and it may also contain a boolean `special` property.
|
||||
It also defines that the response sent may contain a `report` property mapping to an array of strings.
|
||||
|
||||
The following properties are recognized by the Request constructor:
|
||||
The following properties are recognized as part of a message class:
|
||||
|
||||
#### type
|
||||
#### static plugin
|
||||
|
||||
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.
|
||||
Must contain the name of the plugin this message is defined by.
|
||||
This property is not used in core messages.
|
||||
|
||||
#### links
|
||||
#### static type
|
||||
|
||||
An array of strings describing which links this request can be sent over.
|
||||
Direction matters; `"controller-host"` means the request can be sent from the controller to the host and the host can reply to the controller, but the host can't send a request to the controller unless `"host-controller"` is also present in the links array.
|
||||
A string describing the message type sent over the wire.
|
||||
This can be either `"request"` or `"event"`.
|
||||
The primary difference is whether they allow for a response.
|
||||
|
||||
The available endpoints are `controller`, `host`, `instance`, and `control`.
|
||||
Controller talks with host and control, and host 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 controller to instance it must have both `"controller-host"` and `"host-instance"` in the links array).
|
||||
See `forwardTo` for ways to forward a request to the next link in a chain.
|
||||
#### static src
|
||||
|
||||
#### forwardTo
|
||||
A string or an array of strings describing where this message is allowed to originate from.
|
||||
The available endpoints are `"controller"`, `"host"`, `"instance"`, or `"control"`.
|
||||
Messages sent from a different source will be rejected.
|
||||
|
||||
Target to forward the request to.
|
||||
Can either be `"controller"` to indicate a host should forward it to the controller 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.
|
||||
#### static dst
|
||||
|
||||
#### requestRequired
|
||||
A string or array of strings describing where this message may be addressed to.
|
||||
The available endpoints are `"controller"`, `"host"`, `"instance"`, or `"control"`.
|
||||
If the target is multiple hops away it the message will be forwarded towards its destination by the intermediaries.
|
||||
For example a message sent from a control connection an instance will be forwarded by the controller to the relevant host and from the host to the correct instance.
|
||||
Messages sent to a different destination will be rejected.
|
||||
|
||||
By default all properties defined in `requestProperties` are required to be present in the payload for the request.
|
||||
This can be overriden by specifying an array of properties which are required here.
|
||||
This is equivalent to using the `required` keyword in JSON schema.
|
||||
#### static jsonSchema
|
||||
|
||||
#### requestProperties
|
||||
A JSON schema that specifies what's valid to send in the message.
|
||||
|
||||
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 by default required and additional properties are not allowed.
|
||||
See [this guide][guide] for an introduction to writing JSON schemas
|
||||
#### toJSON
|
||||
|
||||
#### responseRequired
|
||||
Optional method to serialize an instance of the class to a JSON object, see the [documentation for `JSON.stringify()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#description) for the semantics of this method.
|
||||
This should return an object that's valid according to `jsonSchema` and accepted by `fromJSON`, and may be omitted if the default serialisation by `JSON.stringify()` already does this.
|
||||
|
||||
Same as the requestRequired only for the response sent back by the target.
|
||||
#### static fromJSON
|
||||
|
||||
#### responseProperties
|
||||
Required function to restore the class from a JSON object.
|
||||
This function is called when the message reaches its destination and is expected to return an identical instance of the class.
|
||||
I.e. the result of `fromJSON(JSON.parse(JSON.stringify(instance)))` should be a complete copy of the message.
|
||||
|
||||
Same as the requestProperties only for the response sent back by the target.
|
||||
#### static Response
|
||||
|
||||
Optional [JSON sterilisable class](/docs/devs/json-serialisable-classes.md) that specifies the response to the request.
|
||||
Required properties are `jsonSchema` and `fromJSON`.
|
||||
|
||||
## Sending Link Messages
|
||||
|
||||
Link messages are sent by calling the `.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 host 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 `plugin` export.
|
||||
In other words:
|
||||
Link messages are sent by calling the `.sendTo()` method on a connection object with the destination and the data you want to send, where the data is an instance of one of the defined message classes in plugin.messages.
|
||||
For example:
|
||||
|
||||
```js
|
||||
// In an InstancePlugin class
|
||||
async frobnicate() {
|
||||
this.info.messages.exampleEvent.send(this.instance, { foo: "bar" });
|
||||
const response = await this.instance.sendTo("controller", new messages.Frobnicate({
|
||||
verbosity: 2,
|
||||
special: false,
|
||||
}));
|
||||
console.log(response); // { report: [...] }
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
For classes with `static type = "request"` the send method is async and returns a promise that resolves to the response data received from the target it was sent to, or rejects with an error if the request failed.
|
||||
The destination specification can either be the ID of a particular control, host or instance, or one of the keywords used to send to multiple targets at once.
|
||||
|
||||
Example destinations:
|
||||
|
||||
```js
|
||||
sendTo("controller", message);
|
||||
sendTo("allHosts", message);
|
||||
sendTo("allControls", message);
|
||||
sendTo("allInstances", message);
|
||||
sendTo({ hostId: 123 }, message);
|
||||
sendTo({ instanceId: 123 }, message);
|
||||
sendTo({ controlId: 123 }, message);
|
||||
```
|
||||
|
||||
### Handling connection events
|
||||
|
||||
@@ -509,7 +469,7 @@ For example the sending of an event from an `InstancePlugin` class can be stoppe
|
||||
|
||||
```js
|
||||
if (this.host.connected) {
|
||||
this.info.messages.frobnicate.send(this.instance, { foo: "bar" });
|
||||
this.instance.sendTo("controller", new messages.Frobnicate({ foo: "bar" }));
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "tsc --build",
|
||||
"watch": "tsc --build --watch",
|
||||
"test": "pnpm run build && mocha --check-leaks -R spec --recursive test \"plugins/*/test/**\"",
|
||||
"fast-test": "FAST_TEST=y pnpm test",
|
||||
"cover": "nyc pnpm test",
|
||||
@@ -27,6 +28,7 @@
|
||||
"@clusterio/ctl": "workspace:*",
|
||||
"@clusterio/host": "workspace:*",
|
||||
"@clusterio/lib": "workspace:*",
|
||||
"@swc/core": "^1.4.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.4.1",
|
||||
"@typescript-eslint/parser": "^6.4.1",
|
||||
"eslint": "^8.45.0",
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
webpack.config.js
|
||||
dist/web/build
|
||||
dist/browser
|
||||
@@ -37,6 +37,24 @@ import { version } from "./package.json";
|
||||
// globals
|
||||
let controller: Controller;
|
||||
|
||||
void new lib.Gauge(
|
||||
"clusterio_controller_pending_requests",
|
||||
"Count of pending link requests currently waiting in memory on the controller.",
|
||||
{
|
||||
callback: (gauge) => {
|
||||
let count = 0;
|
||||
if (controller) {
|
||||
for (const host of controller.wsServer.hostConnections.values()) {
|
||||
count += host.pendingRequestCount;
|
||||
}
|
||||
for (const control of controller.wsServer.controlConnections.values()) {
|
||||
count += control.pendingRequestCount;
|
||||
}
|
||||
}
|
||||
gauge.set(count);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
void new lib.Gauge(
|
||||
"clusterio_controller_host_mapping",
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { bootstrap } from "./controller";
|
||||
export { default as Controller } from "./src/Controller";
|
||||
export { default as ControllerUser } from "./src/ControllerUser";
|
||||
export { default as ControllerRouter } from "./src/ControllerRouter";
|
||||
export { default as ControlConnection } from "./src/ControlConnection";
|
||||
export { default as HostConnection } from "./src/HostConnection";
|
||||
export { default as InstanceInfo } from "./src/InstanceInfo";
|
||||
export { default as BaseControllerPlugin } from "./src/BaseControllerPlugin";
|
||||
export { default as UserManager } from "./src/UserManager";
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
"description": "Implementation of Clusterio controller",
|
||||
"author": "Hornwitser",
|
||||
"license": "MIT",
|
||||
"version": "2.0.0-alpha.14",
|
||||
"main": "dist/index.js",
|
||||
"version": "2.0.0-alpha.17",
|
||||
"main": "dist/node/index.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build && webpack-cli --env production",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"bin": {
|
||||
"clusteriocontroller": "./dist/controller.js"
|
||||
"clusteriocontroller": "./dist/node/controller.js"
|
||||
},
|
||||
"keywords": [
|
||||
"factorio"
|
||||
@@ -19,11 +19,12 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clusterio/lib": "^2.0.0-alpha.14",
|
||||
"@clusterio/lib": "^2.0.0-alpha.17",
|
||||
"@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",
|
||||
"jsonwebtoken": "^9.0.1",
|
||||
"jszip": "^3.10.1",
|
||||
@@ -35,10 +36,12 @@
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@clusterio/web_ui": "^2.0.0-alpha.14",
|
||||
"@clusterio/web_ui": "^2.0.0-alpha.17",
|
||||
"@swc/core": "^1.4.0",
|
||||
"@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",
|
||||
|
||||
@@ -6,7 +6,6 @@ import type WsServerConnector from "./WsServerConnector";
|
||||
|
||||
import * as lib from "@clusterio/lib";
|
||||
const { logger } = lib;
|
||||
import ControllerRouter from "./ControllerRouter";
|
||||
import * as routes from "./routes";
|
||||
|
||||
|
||||
@@ -24,7 +23,7 @@ export default class BaseConnection extends lib.Link {
|
||||
public _controller: Controller
|
||||
) {
|
||||
super(connector);
|
||||
this.router = new ControllerRouter(this._controller);
|
||||
this.router = _controller.router;
|
||||
for (let [Request, handler] of this._controller._registeredRequests) { this.handle(Request, handler); }
|
||||
for (let [Request, handler] of this._controller._fallbackedRequests) { this.fallbackRequest(Request, handler); }
|
||||
for (let [Event, handler] of this._controller._registeredEvents) { this.handle(Event, handler); }
|
||||
|
||||
@@ -3,6 +3,7 @@ import { BlockList, type AddressInfo, isIPv4, isIPv6 } from "net";
|
||||
import type { ControllerArgs } from "../controller";
|
||||
import express, { type Request, type Response, type NextFunction, type Application } from "express";
|
||||
import type { Static } from "@sinclair/typebox";
|
||||
import finalhandler from "finalhandler";
|
||||
|
||||
import compression from "compression";
|
||||
import events, { EventEmitter } from "events";
|
||||
@@ -26,6 +27,7 @@ import WsServer from "./WsServer";
|
||||
import HostConnection from "./HostConnection";
|
||||
import HostInfo from "./HostInfo";
|
||||
import BaseControllerPlugin from "./BaseControllerPlugin";
|
||||
import ControllerRouter from "./ControllerRouter";
|
||||
|
||||
const endpointDurationSummary = new Summary(
|
||||
"clusterio_controller_http_endpoint_duration_seconds",
|
||||
@@ -74,6 +76,7 @@ export default class Controller {
|
||||
|
||||
/** WebSocket server */
|
||||
wsServer: WsServer;
|
||||
router = new ControllerRouter(this);
|
||||
trustedProxies: BlockList;
|
||||
debugEvents: events.EventEmitter = new events.EventEmitter();
|
||||
private _events: events.EventEmitter = new events.EventEmitter();
|
||||
@@ -272,7 +275,7 @@ export default class Controller {
|
||||
Controller.addAppRoutes(this.app, this.pluginInfos);
|
||||
|
||||
if (!args.dev) {
|
||||
let manifestPath = path.join(__dirname, "..", "web", "manifest.json");
|
||||
let manifestPath = path.join(__dirname, "..", "..", "web", "manifest.json");
|
||||
|
||||
let manifest = await Controller.loadJsonObject(manifestPath);
|
||||
if (!manifest["main.js"]) {
|
||||
@@ -284,6 +287,26 @@ export default class Controller {
|
||||
// Load plugins
|
||||
await this.loadPlugins();
|
||||
|
||||
// Log all express errors from middleware or routes (app.use is missing the type overload)
|
||||
this.app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction): void => {
|
||||
logger.log({
|
||||
level: "error",
|
||||
message: "Error while handling HTTP" +
|
||||
`${req.httpVersion} ${req.method} ${req.originalUrl}:\n${err.stack}`,
|
||||
meta: {
|
||||
method: req.method,
|
||||
url: req.originalUrl,
|
||||
httpVersion: req.httpVersion,
|
||||
headers: req.headers,
|
||||
query: req.query,
|
||||
},
|
||||
});
|
||||
|
||||
// Create and call the final handler, this is the same method used by express
|
||||
// This is used instead of next(err) to stop express from logging the error to the console
|
||||
finalhandler(req, res, { env: this.app.get("env") })(err);
|
||||
});
|
||||
|
||||
this.wsServer = new WsServer(this);
|
||||
|
||||
// Only start listening for connections after all plugins have loaded
|
||||
@@ -331,7 +354,7 @@ export default class Controller {
|
||||
|
||||
if (args.dev) {
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
webpackConfigs.push(require("../../webpack.config")({})); // Path outside of build
|
||||
webpackConfigs.push(require("../../../webpack.config")({})); // Path outside of build
|
||||
}
|
||||
if (args.devPlugin) {
|
||||
let devPlugins = new Map();
|
||||
@@ -719,14 +742,29 @@ export default class Controller {
|
||||
|
||||
static addAppRoutes(app: Application, pluginInfos: any[]) {
|
||||
app.use((req: Request, res: Response, next) => {
|
||||
let startNs = process.hrtime.bigint();
|
||||
const startNs = process.hrtime.bigint();
|
||||
stream.finished(res, () => {
|
||||
let routePath = "static";
|
||||
if (req.route && req.route.path) {
|
||||
routePath = req.route.path;
|
||||
}
|
||||
let endNs = process.hrtime.bigint();
|
||||
endpointDurationSummary.labels(routePath).observe(Number(endNs - startNs) / 1e9);
|
||||
const endNs = process.hrtime.bigint();
|
||||
const durationMs = (Number(endNs - startNs) / 1e6);
|
||||
endpointDurationSummary.labels(routePath).observe(durationMs / 1e3);
|
||||
logger.log({
|
||||
level: "http",
|
||||
message: `HTTP${req.httpVersion} ${req.method} ${req.originalUrl}` +
|
||||
` (Status ${res.statusCode} in ${durationMs}ms)`,
|
||||
meta: {
|
||||
method: req.method,
|
||||
url: req.originalUrl,
|
||||
statusCode: res.statusCode,
|
||||
responseTime: durationMs,
|
||||
httpVersion: req.httpVersion,
|
||||
headers: req.headers,
|
||||
query: req.query,
|
||||
},
|
||||
});
|
||||
});
|
||||
next();
|
||||
});
|
||||
@@ -735,7 +773,7 @@ export default class Controller {
|
||||
// Set folder to serve static content from (the website)
|
||||
const staticOptions = { immutable: true, maxAge: 1000 * 86400 * 365 };
|
||||
app.use("/static",
|
||||
express.static(path.join(__dirname, "..", "..", "dist", "web", "static"), staticOptions)
|
||||
express.static(path.join(__dirname, "..", "..", "web", "static"), staticOptions)
|
||||
);
|
||||
app.use("/static", express.static("static", staticOptions)); // Used for data export files
|
||||
|
||||
@@ -874,9 +912,11 @@ export default class Controller {
|
||||
*/
|
||||
clearSavesOfInstance(instanceId: number) {
|
||||
const updates = [];
|
||||
const now = Date.now();
|
||||
for (const [id, save] of this.saves) {
|
||||
if (save.instanceId === instanceId) {
|
||||
save.isDeleted = true;
|
||||
save.updatedAtMs = now;
|
||||
updates.push(save);
|
||||
this.saves.delete(id);
|
||||
}
|
||||
@@ -937,9 +977,9 @@ export default class Controller {
|
||||
);
|
||||
} else {
|
||||
instance.status = "unassigned";
|
||||
instance.updatedAtMs = Date.now();
|
||||
this.instanceDetailsUpdated([instance]);
|
||||
}
|
||||
instance.updatedAtMs = Date.now();
|
||||
this.instanceDetailsUpdated([instance]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -960,6 +1000,7 @@ export default class Controller {
|
||||
this.instancesDirty = true;
|
||||
|
||||
this.clearSavesOfInstance(instanceId);
|
||||
this.userManager.clearStatsOfInstance(instanceId);
|
||||
|
||||
let prev = instance.status;
|
||||
instance.status = "deleted";
|
||||
@@ -1222,7 +1263,7 @@ export default class Controller {
|
||||
mainBundle = stats.toJson().assetsByChunkName["main"];
|
||||
}
|
||||
|
||||
fs.readFile(path.join(__dirname, "..", "..", "web", "index.html"), "utf8").then((content) => {
|
||||
fs.readFile(path.join(__dirname, "..", "..", "..", "web", "index.html"), "utf8").then((content) => {
|
||||
res.type("text/html");
|
||||
res.send(content
|
||||
.replace(/__CLUSTER_NAME__/g, res.app.locals.controller.config.get("controller.name"))
|
||||
@@ -1374,7 +1415,11 @@ export default class Controller {
|
||||
}
|
||||
|
||||
} else if (dst.id === lib.Address.instance || dst.id === lib.Address.host) {
|
||||
const plugin = event.constructor.plugin;
|
||||
for (let hostConnection of this.wsServer.hostConnections.values()) {
|
||||
if (plugin && !hostConnection.plugins.has(plugin)) {
|
||||
continue;
|
||||
}
|
||||
hostConnection.sendEvent(event, dst);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@ import ControlConnection from "./ControlConnection";
|
||||
const { logger } = lib;
|
||||
|
||||
export default class ControllerRouter {
|
||||
pendingRequests: Map<any, any> = new Map();
|
||||
|
||||
constructor(
|
||||
public controller: Controller
|
||||
) {}
|
||||
@@ -16,12 +14,14 @@ export default class ControllerRouter {
|
||||
/**
|
||||
* @param origin - Source link of the message.
|
||||
* @param message - Link the message originated from.
|
||||
* @param entry - message entry if message is a request or an event.
|
||||
* @param hasFallback - true if fallback handling is available.
|
||||
* @returns true if the message was handled
|
||||
*/
|
||||
forwardMessage(
|
||||
origin: ControlConnection | HostConnection,
|
||||
message: lib.MessageRoutable,
|
||||
entry: lib.RequestEntry | lib.EventEntry | undefined,
|
||||
hasFallback: boolean
|
||||
): boolean {
|
||||
if (!["request", "response", "responseError", "event"].includes(message.type)) {
|
||||
@@ -32,7 +32,7 @@ export default class ControllerRouter {
|
||||
let nextHop: ControlConnection | HostConnection | undefined;
|
||||
let msg: string = "";
|
||||
if (dst.type === lib.Address.broadcast) {
|
||||
this.broadcastMessage(origin, message);
|
||||
this.broadcastMessage(origin, message, entry as lib.EventEntry);
|
||||
return true;
|
||||
} else if (dst.type === lib.Address.host) {
|
||||
nextHop = this.controller.wsServer.hostConnections.get(dst.id);
|
||||
@@ -84,7 +84,7 @@ export default class ControllerRouter {
|
||||
if (message.type === "request") {
|
||||
nextHop.forwardRequest(message as lib.MessageRequest, origin);
|
||||
} else {
|
||||
nextHop.connector.send(message);
|
||||
nextHop.connector.forward(message);
|
||||
}
|
||||
} else {
|
||||
this.warnUnrouted(message, msg);
|
||||
@@ -96,20 +96,24 @@ export default class ControllerRouter {
|
||||
broadcastMessage(
|
||||
origin: lib.Link,
|
||||
message: lib.MessageRoutable,
|
||||
entry: lib.EventEntry,
|
||||
) {
|
||||
let dst = message.dst;
|
||||
if (message.type !== "event") {
|
||||
this.warnUnrouted(message, `Unexpected broadcast of ${message.type}`);
|
||||
} else if (dst.id === lib.Address.host || dst.id === lib.Address.instance) {
|
||||
return;
|
||||
}
|
||||
const plugin = entry.Event.plugin;
|
||||
if (dst.id === lib.Address.host || dst.id === lib.Address.instance) {
|
||||
for (let hostConnection of this.controller.wsServer.hostConnections.values()) {
|
||||
if (hostConnection !== origin) {
|
||||
hostConnection.connector.send(message);
|
||||
if (hostConnection !== origin && (!plugin || hostConnection.plugins.has(plugin))) {
|
||||
hostConnection.connector.forward(message);
|
||||
}
|
||||
}
|
||||
} else if (dst.id === lib.Address.control) {
|
||||
for (let controlConnection of this.controller.wsServer.controlConnections.values()) {
|
||||
if (controlConnection !== origin) {
|
||||
controlConnection.connector.send(message);
|
||||
controlConnection.connector.forward(message);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -126,5 +130,3 @@ export default class ControllerRouter {
|
||||
logger.warn(msg ? `${baseMsg}: ${msg}.` : `${baseMsg}.`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ControllerRouter;
|
||||
@@ -178,7 +178,10 @@ export default class HostConnection extends BaseConnection {
|
||||
// changed. Hosts also send status updates on assignInstance which
|
||||
// for hacky reason is also used to push config changes and
|
||||
// restablish status after a connection loss.
|
||||
if (instance.status === request.status) {
|
||||
if (
|
||||
instance.status === request.status
|
||||
&& instance.gamePort === request.gamePort
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -113,4 +113,13 @@ export default class UserManager {
|
||||
this.onlineUsers.delete(user);
|
||||
}
|
||||
}
|
||||
|
||||
clearStatsOfInstance(instanceId: number) {
|
||||
for (const user of this.users.values()) {
|
||||
this.notifyLeave(user, instanceId);
|
||||
user.instanceStats.delete(instanceId);
|
||||
user.recalculatePlayerStats();
|
||||
}
|
||||
this.dirty = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.browser.json",
|
||||
"references": [
|
||||
{ "path": "../lib/tsconfig.browser.json" },
|
||||
{ "path": "../web_ui/tsconfig.browser.json" },
|
||||
],
|
||||
"include": [ "web/**/*.tsx", "web/**/*.ts", "package.json" ],
|
||||
"compilerOptions": {
|
||||
"rootDir": "..",
|
||||
"outDir": "dist/browser",
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.node.json",
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "../lib" },
|
||||
{ "path": "./tsconfig.node.json" },
|
||||
{ "path": "./tsconfig.browser.json" },
|
||||
],
|
||||
"include": ["index.ts", "controller.ts", "package.json", "src/**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.node.json",
|
||||
"references": [
|
||||
{ "path": "../lib/tsconfig.node.json" },
|
||||
],
|
||||
"include": ["index.ts", "controller.ts", "package.json", "src/**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/node",
|
||||
},
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.web.json",
|
||||
"references": [
|
||||
{ "path": "../lib/tsconfig.web.json" },
|
||||
{ "path": "../web_ui/tsconfig.web.json" },
|
||||
],
|
||||
"include": [ "web/**/*.tsx", "web/**/*.ts", "package.json" ],
|
||||
"compilerOptions": {
|
||||
"rootDir": "..",
|
||||
"outDir": "dist/web/build",
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
"description": "Installer for clusterio",
|
||||
"author": "Hornwitser",
|
||||
"license": "MIT",
|
||||
"version": "2.0.0-alpha.14",
|
||||
"version": "2.0.0-alpha.17",
|
||||
"main": "create.js",
|
||||
"bin": {
|
||||
"clusteriocreate": "./create.js"
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
"description": "Clusterio command line management tool",
|
||||
"author": "Hornwitser",
|
||||
"license": "MIT",
|
||||
"version": "2.0.0-alpha.14",
|
||||
"main": "dist/index.js",
|
||||
"version": "2.0.0-alpha.17",
|
||||
"main": "dist/node/index.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"bin": {
|
||||
"clusterioctl": "./dist/ctl.js"
|
||||
"clusterioctl": "./dist/node/ctl.js"
|
||||
},
|
||||
"keywords": [
|
||||
"factorio"
|
||||
@@ -19,7 +19,7 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clusterio/lib": "^2.0.0-alpha.14",
|
||||
"@clusterio/lib": "^2.0.0-alpha.17",
|
||||
"as-table": "^1.0.55",
|
||||
"fs-extra": "^11.1.1",
|
||||
"phin": "^3.7.0",
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
{
|
||||
"extends": "../../tsconfig.node.json",
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "../lib" },
|
||||
{ "path": "./tsconfig.node.json" },
|
||||
],
|
||||
"include": ["index.ts", "package.json", "ctl.ts", "src/**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.node.json",
|
||||
"references": [
|
||||
{ "path": "../lib/tsconfig.node.json" },
|
||||
],
|
||||
"include": ["index.ts", "package.json", "ctl.ts", "src/**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/node",
|
||||
},
|
||||
}
|
||||
+26
-5
@@ -26,6 +26,27 @@ import { ConsoleTransport, levels, logger } from "@clusterio/lib";
|
||||
|
||||
import Host from "./src/Host";
|
||||
|
||||
let host: Host | undefined;
|
||||
|
||||
void new lib.Gauge(
|
||||
"clusterio_host_pending_requests",
|
||||
"Count of pending link requests currently waiting in memory on the host.",
|
||||
{
|
||||
labels: ["host_id"],
|
||||
callback: (gauge) => {
|
||||
if (!host) {
|
||||
return;
|
||||
}
|
||||
|
||||
let count = host.pendingRequestCount;
|
||||
for (const connection of host.instanceConnections.values()) {
|
||||
count += connection.pendingRequestCount;
|
||||
count += connection.instance.pendingRequestCount;
|
||||
}
|
||||
gauge.labels(String(host.config.get("host.id"))).set(count);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export class HostConnector extends lib.WebSocketClientConnector {
|
||||
constructor(
|
||||
@@ -205,7 +226,7 @@ async function startHost() {
|
||||
}
|
||||
|
||||
let hostConnector = new HostConnector(hostConfig, tlsCa, pluginInfos);
|
||||
let host = new Host(
|
||||
host = new Host(
|
||||
hostConnector,
|
||||
args.config,
|
||||
hostConfig,
|
||||
@@ -227,7 +248,7 @@ async function startHost() {
|
||||
|
||||
secondSigint = true;
|
||||
logger.info("Caught interrupt signal, shutting down");
|
||||
host.shutdown();
|
||||
host!.shutdown();
|
||||
});
|
||||
let secondSigterm = false;
|
||||
process.on("SIGTERM", () => {
|
||||
@@ -240,11 +261,11 @@ async function startHost() {
|
||||
|
||||
secondSigterm = true;
|
||||
logger.info("Caught termination signal, shutting down");
|
||||
host.shutdown();
|
||||
host!.shutdown();
|
||||
});
|
||||
process.on("SIGHUP", () => {
|
||||
logger.info("Terminal closed, shutting down");
|
||||
host.shutdown();
|
||||
host!.shutdown();
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -255,7 +276,7 @@ async function startHost() {
|
||||
}
|
||||
|
||||
hostConnector.once("connect", () => {
|
||||
logger.add(new lib.LinkTransport({ link: host }));
|
||||
logger.add(new lib.LinkTransport({ link: host! }));
|
||||
});
|
||||
|
||||
await hostConnector.connect();
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { bootstrap } from "./host";
|
||||
export { default as BaseHostPlugin } from "./src/BaseHostPlugin";
|
||||
export { default as BaseInstancePlugin } from "./src/BaseInstancePlugin";
|
||||
export { default as Host, HostRouter } from "./src/Host";
|
||||
export { default as Instance } from "./src/Instance";
|
||||
export { default as InstanceConnection } from "./src/InstanceConnection";
|
||||
|
||||
if (module === require.main) {
|
||||
bootstrap();
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
"description": "Implementation of Clusterio host server",
|
||||
"author": "Hornwitser",
|
||||
"license": "MIT",
|
||||
"version": "2.0.0-alpha.14",
|
||||
"main": "dist/index.js",
|
||||
"version": "2.0.0-alpha.17",
|
||||
"main": "dist/node/index.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"bin": {
|
||||
"clusteriohost": "./dist/host.js"
|
||||
"clusteriohost": "./dist/node/host.js"
|
||||
},
|
||||
"keywords": [
|
||||
"factorio"
|
||||
@@ -19,7 +19,7 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clusterio/lib": "^2.0.0-alpha.14",
|
||||
"@clusterio/lib": "^2.0.0-alpha.17",
|
||||
"@sinclair/typebox": "^0.30.4",
|
||||
"fs-extra": "^11.1.1",
|
||||
"jimp": "^0.22.8",
|
||||
|
||||
+32
-16
@@ -82,7 +82,7 @@ const instanceStartingMessages = new Set([
|
||||
lib.InstanceExportDataRequest.name,
|
||||
]);
|
||||
|
||||
class HostRouter {
|
||||
export class HostRouter {
|
||||
constructor(
|
||||
public host: Host
|
||||
) { }
|
||||
@@ -93,16 +93,22 @@ class HostRouter {
|
||||
* @param origin -
|
||||
* Link the message originated from.
|
||||
* @param message - Message to process.
|
||||
* @param entry - message entry if message is a request or an event.
|
||||
* @param hasFallback - is fallback available
|
||||
* @returns true if the message was handled, false if fallback
|
||||
* is requested
|
||||
*/
|
||||
forwardMessage(origin: lib.Link, message: lib.MessageRoutable, hasFallback: boolean) {
|
||||
forwardMessage(
|
||||
origin: lib.Link,
|
||||
message: lib.MessageRoutable,
|
||||
entry: lib.RequestEntry | lib.EventEntry | undefined,
|
||||
hasFallback: boolean,
|
||||
) {
|
||||
let dst = message.dst;
|
||||
let nextHop: lib.Link | undefined;
|
||||
let msg: string | undefined;
|
||||
if (dst.type === lib.Address.broadcast) {
|
||||
this.broadcastMessage(origin, message);
|
||||
this.broadcastMessage(origin, message, entry as lib.EventEntry);
|
||||
return true;
|
||||
} else if (
|
||||
dst.type === lib.Address.controller
|
||||
@@ -153,27 +159,36 @@ class HostRouter {
|
||||
return true;
|
||||
}
|
||||
|
||||
broadcastMessage(origin: lib.Link, message: lib.MessageRoutable) {
|
||||
broadcastMessage(
|
||||
origin: lib.Link,
|
||||
message: lib.MessageRoutable,
|
||||
entry: lib.EventEntry,
|
||||
) {
|
||||
let dst = message.dst;
|
||||
if (message.type !== "event") {
|
||||
this.warnUnrouted(message, `Unexpected broadcast of ${message.type}`);
|
||||
} else if (dst.id === lib.Address.host || dst.id === lib.Address.instance) {
|
||||
return;
|
||||
}
|
||||
const plugin = entry.Event.plugin;
|
||||
if (dst.id === lib.Address.instance) {
|
||||
for (let instanceConnection of this.host.instanceConnections.values()) {
|
||||
if (instanceConnection !== origin) {
|
||||
instanceConnection.connector.send(message);
|
||||
if (instanceConnection !== origin && (!plugin || instanceConnection.plugins.has(plugin))) {
|
||||
instanceConnection.connector.forward(message);
|
||||
}
|
||||
}
|
||||
if (this.host !== origin) {
|
||||
this.host.connector.send(message);
|
||||
if (this.host !== origin && (!plugin || this.host.serverPlugins.has(plugin))) {
|
||||
this.host.connector.forward(message);
|
||||
}
|
||||
} else if (dst.id === lib.Address.control) {
|
||||
if (this.host !== origin) {
|
||||
this.host.connector.send(message);
|
||||
if (!plugin || this.host.serverPlugins.has(plugin)) {
|
||||
this.host.connector.forward(message);
|
||||
}
|
||||
} else {
|
||||
logger.warn(`Received control broadcast of ${(message as lib.MessageEvent).name} from master.`);
|
||||
logger.warn(`Received control broadcast of ${(message as lib.MessageEvent).name} from controller.`);
|
||||
}
|
||||
} else {
|
||||
this.warnUnrouted(message, `Unexpected broacdast target ${dst.id}`);
|
||||
this.warnUnrouted(message, `Unexpected broadcast target ${dst.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,7 +208,7 @@ class HostRouter {
|
||||
}
|
||||
|
||||
this.host._connectInstance(dst.id).then(instanceConnection => {
|
||||
instanceConnection.connector.send(message);
|
||||
instanceConnection.connector.forward(message);
|
||||
}).catch(err => {
|
||||
logger.error(`Error starting instance:\n${err.stack}`, this.host.instanceLogMeta(dst.id));
|
||||
origin.connector.sendResponseError(
|
||||
@@ -207,7 +222,7 @@ class HostRouter {
|
||||
if (message.type === "request") {
|
||||
nextHop.forwardRequest(message as lib.MessageRequest, origin);
|
||||
} else {
|
||||
nextHop.connector.send(message);
|
||||
nextHop.connector.forward(message);
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (message.type === "request") {
|
||||
@@ -585,8 +600,9 @@ export default class Host extends lib.Link {
|
||||
try {
|
||||
modInfo = await this.modStore.loadMod(mod.name, mod.version);
|
||||
} catch (err: any) {
|
||||
// A broken zip file may have been left in the mods folder
|
||||
if (err.code !== "ENOENT") {
|
||||
throw err;
|
||||
logger.error(`Error loading mod ${mod.name} ${mod.version}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
if (modInfo && (!mod.sha1 || mod.sha1 === modInfo.sha1)) {
|
||||
@@ -1056,7 +1072,7 @@ export default class Host extends lib.Link {
|
||||
if (instanceInfo.config.get("instance.auto_start")) {
|
||||
try {
|
||||
let instanceConnection = await this._connectInstance(instanceId);
|
||||
await instanceConnection.send(new lib.InstanceStartRequest());
|
||||
await instanceConnection.instance.handleInstanceStartRequest(new lib.InstanceStartRequest());
|
||||
} catch (err: any) {
|
||||
logger.error(
|
||||
`Error during auto startup for ${instanceInfo.config.get("instance.name")}:\n${err.stack}`,
|
||||
|
||||
@@ -800,7 +800,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
|
||||
// Find stand alone modules to load
|
||||
// XXX for now only the included clusterio module is loaded
|
||||
let modulesDirectory = path.join(__dirname, "..", "..", "modules");
|
||||
let modulesDirectory = path.join(__dirname, "..", "..", "..", "modules");
|
||||
for (let entry of await fs.readdir(modulesDirectory, { withFileTypes: true })) {
|
||||
if (entry.isDirectory()) {
|
||||
if (modules.has(entry.name)) {
|
||||
@@ -955,6 +955,11 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
await lib.invokeHook(this.plugins, "onStop");
|
||||
await this.server.stop();
|
||||
await this.sendSaveListUpdate();
|
||||
} else if (
|
||||
this.server._state === "stopping"
|
||||
|| this.server._state === "create"
|
||||
) {
|
||||
await this.server.kill();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+19
-13
@@ -5,7 +5,7 @@ import Jimp from "jimp";
|
||||
import JSZip from "jszip";
|
||||
|
||||
import * as lib from "@clusterio/lib";
|
||||
import * as libBuildMod from "@clusterio/lib/dist/build_mod";
|
||||
import * as libBuildMod from "@clusterio/lib/dist/node/build_mod";
|
||||
import type { FactorioServer } from "./server";
|
||||
|
||||
interface Prototype {
|
||||
@@ -18,11 +18,10 @@ interface SimpleIconSpecification {
|
||||
icon_size: number;
|
||||
icon_mipmaps?: number;
|
||||
}
|
||||
type Color = { r: number, g: number, b: number, a: number };
|
||||
interface IconLayer {
|
||||
icon: string;
|
||||
icon_size: number;
|
||||
tint?: Color,
|
||||
tint?: lib.FactorioColor,
|
||||
shift?: [number, number],
|
||||
scale?: number,
|
||||
icon_mipmaps?: number,
|
||||
@@ -62,7 +61,7 @@ async function generateExportMod(server: FactorioServer) {
|
||||
clean: false,
|
||||
build: true,
|
||||
pack: true,
|
||||
sourceDir: path.join(__dirname, "..", "..", "lua", "export"),
|
||||
sourceDir: path.join(__dirname, "..", "..", "..", "lua", "export"),
|
||||
outputDir: server.writePath("mods"),
|
||||
bumpPatch: false,
|
||||
factorioVersion: server.version.replace(/\.\d+$/, ""),
|
||||
@@ -202,15 +201,9 @@ async function loadLayeredIcon(
|
||||
|
||||
iconLayer = iconLayer.clone();
|
||||
|
||||
let tint: Color;
|
||||
let tint: lib.Color;
|
||||
if (layer.tint) {
|
||||
let divisor = (layer.tint.r > 1 || layer.tint.g > 1 || layer.tint.b > 1) ? 255 : 1;
|
||||
tint = {
|
||||
r: (layer.tint.r || 0) / divisor,
|
||||
g: (layer.tint.g || 0) / divisor,
|
||||
b: (layer.tint.b || 0) / divisor,
|
||||
a: layer.tint.a || 1,
|
||||
};
|
||||
tint = lib.normalizeColor(layer.tint);
|
||||
} else {
|
||||
tint = { r: 1, b: 1, g: 1, a: 1 };
|
||||
}
|
||||
@@ -252,6 +245,18 @@ async function loadLayeredIcon(
|
||||
return icon;
|
||||
}
|
||||
|
||||
function fixIcons(item: ItemPrototype) {
|
||||
const icons = item.icons;
|
||||
if (typeof icons === "object" && !(icons instanceof Array) && icons !== null) {
|
||||
// It's possible to specify icons as an object with arbitrary keys
|
||||
// and the game will still accept it, cast the value to array if
|
||||
// this is the case.
|
||||
item = { ...item };
|
||||
item.icons = Object.values(icons);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
const itemTypes = new Set([
|
||||
"item",
|
||||
"ammo",
|
||||
@@ -284,7 +289,8 @@ const itemTypes = new Set([
|
||||
function filterItems(prototypes: Prototypes): ItemPrototype[] {
|
||||
return Object.entries(prototypes)
|
||||
.filter(([type, _]) => itemTypes.has(type))
|
||||
.flatMap(([_, typePrototypes]) => Object.values(typePrototypes)) as ItemPrototype[]
|
||||
.flatMap(([_, typePrototypes]) => Object.values(typePrototypes) as ItemPrototype[])
|
||||
.map(fixIcons)
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
@@ -872,7 +872,9 @@ export class FactorioServer extends events.EventEmitter {
|
||||
|
||||
try {
|
||||
let [code, signal] = await events.once(this._server, "exit");
|
||||
void signal;
|
||||
if (signal) {
|
||||
throw new Error(`Factorio exited with signal ${signal}`);
|
||||
}
|
||||
if (code !== 0) {
|
||||
throw new Error(`Factorio exited with status ${code}`);
|
||||
}
|
||||
@@ -1099,7 +1101,7 @@ export class FactorioServer extends events.EventEmitter {
|
||||
* process.
|
||||
*/
|
||||
async kill(unexpected = false) {
|
||||
this._check(["running", "stopping"]);
|
||||
this._check(["running", "stopping", "create"]);
|
||||
this._killed = true;
|
||||
if (!unexpected) {
|
||||
this._state = "stopping";
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
{
|
||||
"extends": "../../tsconfig.node.json",
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "../lib" },
|
||||
{ "path": "./tsconfig.node.json" },
|
||||
],
|
||||
"include": ["index.ts", "host.ts", "package.json", "src/**/*.ts", "src/**/*.js"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.node.json",
|
||||
"references": [
|
||||
{ "path": "../lib/tsconfig.node.json" },
|
||||
],
|
||||
"include": ["index.ts", "host.ts", "package.json", "src/**/*.ts", "src/**/*.js"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/node",
|
||||
},
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
dist/web/build
|
||||
dist/browser
|
||||
@@ -6,6 +6,7 @@ export * from "./src/factorio/exchange_string";
|
||||
export * from "./src/helpers";
|
||||
export * from "./src/ini";
|
||||
export * from "./src/link";
|
||||
export * from "./src/lua_tools";
|
||||
export * from "./src/logging";
|
||||
export * from "./src/permissions";
|
||||
export * from "./src/plugin";
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "@clusterio/lib",
|
||||
"description": "Shared library for Clusterio",
|
||||
"version": "2.0.0-alpha.14",
|
||||
"version": "2.0.0-alpha.17",
|
||||
"repository": "https://github.com/clusterio/clusterio",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build && pnpm run compile_validator",
|
||||
"compile_validator": "node scripts/compile_validator.js"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"main": "dist/node/index.js",
|
||||
"browser": "browser.ts",
|
||||
"keywords": [
|
||||
"factorio"
|
||||
|
||||
@@ -7,9 +7,9 @@ const standaloneCode = require("ajv/dist/standalone").default;
|
||||
/* eslint-disable node/no-sync */
|
||||
|
||||
// Provide an empty file for message_core to load
|
||||
fs.outputFileSync(path.join(__dirname, "../dist/src/data/message_validate.js"), "");
|
||||
fs.outputFileSync(path.join(__dirname, "../dist/node/src/data/message_validate.js"), "");
|
||||
|
||||
const { Message } = require("../dist/src/data/messages_core");
|
||||
const { Message } = require("../dist/node/src/data/messages_core");
|
||||
|
||||
const ajv = new Ajv({
|
||||
allowUnionTypes: true,
|
||||
@@ -28,5 +28,5 @@ function write(output) {
|
||||
`// generated by scripts/compile_validator.js\n// @ts-nocheck\n/* eslint-disable */\n${moduleCode}`
|
||||
);
|
||||
}
|
||||
write("../dist/src/data/message_validate.js");
|
||||
write("../dist/node/src/data/message_validate.js");
|
||||
write("../src/data/message_validate.js");
|
||||
@@ -459,7 +459,7 @@ export class Config<
|
||||
setProp<Field extends keyof Fields & string>(
|
||||
name: Field,
|
||||
prop: string,
|
||||
value: unknown,
|
||||
value?: unknown,
|
||||
location = this.location,
|
||||
) {
|
||||
let def = this.constructor.fieldDefinitions[name];
|
||||
|
||||
@@ -2,6 +2,7 @@ import zlib from "zlib";
|
||||
import { Type, Static } from "@sinclair/typebox";
|
||||
|
||||
import * as libSchema from "../schema";
|
||||
import * as libLuaTools from "../lua_tools";
|
||||
|
||||
import ExportManifest from "./ExportManifest";
|
||||
import { integerFactorioVersion } from "./version";
|
||||
@@ -363,7 +364,11 @@ export default class ModPack {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.settings[settingType].set(prototype.name, { value: prototype.default_value });
|
||||
let value = prototype.default_value;
|
||||
if (prototype.type === "color-setting") {
|
||||
value = libLuaTools.normalizeColor(value);
|
||||
}
|
||||
this.settings[settingType].set(prototype.name, { value });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,12 +19,20 @@ export default class Role {
|
||||
permissions: Type.Array(Type.String()),
|
||||
});
|
||||
|
||||
static remapPermission(permission: string) {
|
||||
// migrate: core.instance.save.list.subscribe was renamed in alpha 17
|
||||
if (permission === "core.instance.save.list.subscribe") {
|
||||
return "core.instance.save.subscribe";
|
||||
}
|
||||
return permission;
|
||||
}
|
||||
|
||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||
return new this(
|
||||
json.id,
|
||||
json.name,
|
||||
json.description,
|
||||
new Set(json.permissions),
|
||||
new Set(json.permissions.map(this.remapPermission)),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ export class Address {
|
||||
}
|
||||
|
||||
toString() {
|
||||
const typeMap = ["controller", "instance", "host", "control"];
|
||||
const typeMap = ["controller", "host", "instance", "control"];
|
||||
const type = typeMap[this.type] || this.type;
|
||||
if (this.type === Address.broadcast) {
|
||||
return `[Address ${type}:${typeMap[this.id] || this.id}]`;
|
||||
|
||||
@@ -208,7 +208,7 @@ export class InstanceConfigSetPropRequest {
|
||||
"instanceId": Type.Integer(),
|
||||
"field": Type.String(),
|
||||
"prop": Type.String(),
|
||||
"value": Type.Unknown(),
|
||||
"value": Type.Optional(Type.Unknown()),
|
||||
});
|
||||
|
||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||
@@ -361,7 +361,7 @@ export class InstanceSaveDetailsUpdatesEvent {
|
||||
static type = "event" as const;
|
||||
static src = ["instance", "host", "controller"] as const;
|
||||
static dst = ["controller", "control"] as const;
|
||||
static permission = "core.instance.save.list.subscribe" as const;
|
||||
static permission = "core.instance.save.subscribe" as const;
|
||||
|
||||
constructor(
|
||||
public updates: SaveDetails[],
|
||||
@@ -385,7 +385,7 @@ export class InstanceSaveDetailsUpdatesEvent {
|
||||
export class InstanceCreateSaveRequest {
|
||||
declare ["constructor"]: typeof InstanceCreateSaveRequest;
|
||||
static type = "request" as const;
|
||||
static src = "control" as const;
|
||||
static src = ["control", "controller"] as const;
|
||||
static dst = "instance" as const;
|
||||
static permission = "core.instance.save.create" as const;
|
||||
|
||||
@@ -596,7 +596,7 @@ export class InstancePushSaveRequest {
|
||||
export class InstanceLoadScenarioRequest {
|
||||
declare ["constructor"]: typeof InstanceLoadScenarioRequest;
|
||||
static type = "request" as const;
|
||||
static src = "control" as const;
|
||||
static src = ["control", "controller"] as const;
|
||||
static dst = "instance" as const;
|
||||
static permission = "core.instance.load_scenario" as const;
|
||||
|
||||
@@ -622,7 +622,7 @@ export class InstanceLoadScenarioRequest {
|
||||
export class InstanceExportDataRequest {
|
||||
declare ["constructor"]: typeof InstanceExportDataRequest;
|
||||
static type = "request" as const;
|
||||
static src = "control" as const;
|
||||
static src = ["control", "controller"] as const;
|
||||
static dst = "instance" as const;
|
||||
static permission = "core.instance.export_data" as const;
|
||||
}
|
||||
@@ -630,7 +630,7 @@ export class InstanceExportDataRequest {
|
||||
export class InstanceExtractPlayersRequest {
|
||||
declare ["constructor"]: typeof InstanceExtractPlayersRequest;
|
||||
static type = "request" as const;
|
||||
static src = "control" as const;
|
||||
static src = ["control", "controller"] as const;
|
||||
static dst = "instance" as const;
|
||||
static permission = "core.instance.extract_players" as const;
|
||||
}
|
||||
@@ -638,7 +638,7 @@ export class InstanceExtractPlayersRequest {
|
||||
export class InstanceStopRequest {
|
||||
declare ["constructor"]: typeof InstanceStopRequest;
|
||||
static type = "request" as const;
|
||||
static src = "control" as const;
|
||||
static src = ["control", "controller"] as const;
|
||||
static dst = "instance" as const;
|
||||
static permission = "core.instance.stop" as const;
|
||||
}
|
||||
@@ -646,7 +646,7 @@ export class InstanceStopRequest {
|
||||
export class InstanceKillRequest {
|
||||
declare ["constructor"]: typeof InstanceKillRequest;
|
||||
static type = "request" as const;
|
||||
static src = "control" as const;
|
||||
static src = ["control", "controller"] as const;
|
||||
static dst = "instance" as const;
|
||||
static permission = "core.instance.kill" as const;
|
||||
}
|
||||
@@ -693,7 +693,7 @@ export class InstanceDeleteInternalRequest {
|
||||
export class InstanceSendRconRequest {
|
||||
declare ["constructor"]: typeof InstanceSendRconRequest;
|
||||
static type = "request" as const;
|
||||
static src = "control" as const;
|
||||
static src = ["control", "controller"] as const;
|
||||
static dst = "instance" as const;
|
||||
static permission = "core.instance.send_rcon" as const;
|
||||
|
||||
|
||||
@@ -33,7 +33,30 @@ export abstract class BaseConnector extends events.EventEmitter {
|
||||
this._seq = 1;
|
||||
}
|
||||
|
||||
abstract send(message: libData.Message): void;
|
||||
protected abstract send(message: libData.MessageRoutable): void;
|
||||
|
||||
forward(message: libData.MessageRoutable) {
|
||||
const seq = this._seq;
|
||||
this._seq += 1;
|
||||
// The message type currently can't be inferred like a tagged union
|
||||
// TODO fix typing of message so it can be deduced from .type
|
||||
if (message.type === "request") {
|
||||
const request = message as libData.MessageRequest;
|
||||
this.send(new libData.MessageRequest(seq, request.src, request.dst, request.name, request.data));
|
||||
} else if (message.type === "response") {
|
||||
const response = message as libData.MessageResponse;
|
||||
this.send(new libData.MessageResponse(seq, response.src, response.dst, response.data));
|
||||
} else if (message.type === "responseError") {
|
||||
const error = message as libData.MessageResponseError;
|
||||
this.send(new libData.MessageResponseError(seq, error.src, error.dst, error.data));
|
||||
} else if (message.type === "event") {
|
||||
const event = message as libData.MessageEvent;
|
||||
this.send(new libData.MessageEvent(seq, event.src, event.dst, event.name, event.data));
|
||||
} else {
|
||||
throw new Error(`Cannot forward message type ${(message as any).type}`);
|
||||
}
|
||||
return seq;
|
||||
}
|
||||
|
||||
sendRequest<Req, Res>(
|
||||
request: Request<Req, Res>,
|
||||
@@ -242,7 +265,7 @@ export abstract class WebSocketBaseConnector extends BaseConnector {
|
||||
*
|
||||
* @param message - Message to send.
|
||||
*/
|
||||
send(message: libData.MessageRoutable) {
|
||||
protected send(message: libData.MessageRoutable) {
|
||||
if (!["connected", "resuming"].includes(this._state)) {
|
||||
throw new libErrors.SessionLost("No session");
|
||||
}
|
||||
@@ -295,6 +318,13 @@ export abstract class WebSocketBaseConnector extends BaseConnector {
|
||||
await this.close(1000, "Disconnect");
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify other end that the link is ready to disconnect
|
||||
*/
|
||||
sendDisconnectReady() {
|
||||
this._sendInternal(new libData.MessageDisconnect("ready"));
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the connection is established and active
|
||||
*/
|
||||
|
||||
@@ -72,7 +72,12 @@ export type RequestHandler<Req, Res> = (request: Req, src: libData.Address, dst:
|
||||
export type EventHandler<T> = (event: T, src: libData.Address, dst: libData.Address) => Promise<void>;
|
||||
|
||||
interface Router {
|
||||
forwardMessage(origin: Link, message: MessageRoutable, fallback: boolean): boolean,
|
||||
forwardMessage(
|
||||
origin: Link,
|
||||
message: MessageRoutable,
|
||||
entry: RequestEntry | EventEntry | undefined,
|
||||
fallback: boolean,
|
||||
): boolean,
|
||||
}
|
||||
|
||||
// Some definitions for the terminology used here:
|
||||
@@ -102,6 +107,9 @@ export class Link {
|
||||
) {
|
||||
this.handle(libData.PingRequest, async () => {});
|
||||
|
||||
// Prevent warnings about possible memory leak due to large number of event listeners
|
||||
connector.setMaxListeners(20);
|
||||
|
||||
// Process messages received by the connector
|
||||
connector.on("message", payload => {
|
||||
try {
|
||||
@@ -121,15 +129,15 @@ export class Link {
|
||||
});
|
||||
|
||||
connector.on("disconnectPrepare", () => {
|
||||
this.prepareDisconnect().catch(
|
||||
err => {
|
||||
logger.error(`Unexpected error preparing disconnect:\n${err.stack}`);
|
||||
this.prepareDisconnect().finally(() => {
|
||||
// Only WebSocket connectors issue disconnection events.
|
||||
const webSocketConnector = this.connector as WebSocketBaseConnector;
|
||||
if (webSocketConnector.hasSession) {
|
||||
webSocketConnector.sendDisconnectReady();
|
||||
}
|
||||
).finally(
|
||||
() => {
|
||||
this.connector.send(new libData.MessageDisconnect("ready"));
|
||||
},
|
||||
);
|
||||
}).catch(err => {
|
||||
logger.error(`Unexpected error preparing disconnect:\n${err.stack}`);
|
||||
});
|
||||
});
|
||||
|
||||
connector.on("invalidate", () => {
|
||||
@@ -141,17 +149,30 @@ export class Link {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Count of requests currently waiting for a response on this link
|
||||
*/
|
||||
get pendingRequestCount() {
|
||||
return this._pendingRequests.size + this._forwardedRequests.size;
|
||||
}
|
||||
|
||||
_clearPendingRequests(err: Error & { code?: string }) {
|
||||
for (let pending of this._pendingRequests.values()) {
|
||||
pending.reject(err);
|
||||
}
|
||||
for (let pending of this._forwardedRequests.values()) {
|
||||
assert(pending.origin.connector instanceof WebSocketBaseConnector);
|
||||
if (pending.origin.connector.hasSession) {
|
||||
pending.origin.connector.sendResponseError(
|
||||
new libData.ResponseError(err.message, err.code), pending.src, pending.dst
|
||||
);
|
||||
// Drop response if the origin is a WebSocket and the session
|
||||
// has expired. The other end of the link will have sent an
|
||||
// error response to the request in this case.
|
||||
if (
|
||||
pending.origin.connector instanceof WebSocketBaseConnector
|
||||
&& !pending.origin.connector.hasSession
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
pending.origin.connector.sendResponseError(
|
||||
new libData.ResponseError(err.message, err.code), pending.src, pending.dst
|
||||
);
|
||||
}
|
||||
this._pendingRequests.clear();
|
||||
this._forwardedRequests.clear();
|
||||
@@ -330,7 +351,7 @@ export class Link {
|
||||
let fallback =
|
||||
message.type === "request" ? this._requestFallbacks.get((entry as RequestEntry).Request) : undefined
|
||||
;
|
||||
if (this.router.forwardMessage(this, message, Boolean(fallback))) {
|
||||
if (this.router.forwardMessage(this, message, entry, Boolean(fallback))) {
|
||||
return;
|
||||
}
|
||||
if (!fallback) {
|
||||
@@ -421,6 +442,7 @@ export class Link {
|
||||
throw new libErrors.InvalidMessage(`Received reply from ${message.src} for message sent to ${pending.dst}`);
|
||||
}
|
||||
|
||||
this._pendingRequests.delete(message.dst.requestId!);
|
||||
try {
|
||||
pending.resolve(pending.request.responseFromJSON(message.data));
|
||||
} catch (err: any) {
|
||||
@@ -437,6 +459,7 @@ export class Link {
|
||||
);
|
||||
}
|
||||
|
||||
this._pendingRequests.delete(message.dst.requestId!);
|
||||
pending.reject(new libErrors.RequestError(message.data.message, message.data.code, message.data.stack));
|
||||
}
|
||||
|
||||
@@ -541,7 +564,7 @@ export class Link {
|
||||
dst: message.dst,
|
||||
};
|
||||
this._forwardedRequests.set(message.src.index(), pending);
|
||||
this.connector.send(message);
|
||||
this.connector.forward(message);
|
||||
}
|
||||
|
||||
sendEvent<T>(event: Event<T>, dst: libData.Address) {
|
||||
|
||||
@@ -8,8 +8,9 @@ export const levels = Object.freeze({
|
||||
warn: 2,
|
||||
audit: 3,
|
||||
info: 4,
|
||||
server: 5,
|
||||
verbose: 6,
|
||||
http: 5,
|
||||
server: 6,
|
||||
verbose: 7,
|
||||
});
|
||||
|
||||
export const colors = {
|
||||
@@ -18,6 +19,7 @@ export const colors = {
|
||||
warn: "bold brightYellow",
|
||||
audit: "bold brightGreen",
|
||||
info: "bold brightBlue",
|
||||
http: "bold",
|
||||
server: "bold",
|
||||
verbose: "bold grey",
|
||||
} as const;
|
||||
@@ -73,7 +75,7 @@ export class WebConsoleFormat {
|
||||
transform(info: any, _options: object) {
|
||||
let src = " ";
|
||||
if (info.host_id !== undefined) {
|
||||
src += `s:${info.host_name} - `;
|
||||
src += `h:${info.host_name} - `;
|
||||
}
|
||||
if (info.instance_id !== undefined) {
|
||||
src += `i:${info.instance_name}`;
|
||||
@@ -163,7 +165,7 @@ export const logger = winston.createLogger({
|
||||
// Who in their right mind thought that log levels should be hard coded
|
||||
// into the type when you can define custom levels you want to support?
|
||||
export type Logger = Omit<winston.Logger,
|
||||
"error" | "warn" | "help" | "data" | "info" | "debug" | "prompt" | "http" | "verbose" | "input" | "silly" |
|
||||
"error" | "warn" | "help" | "data" | "info" | "debug" | "prompt" | "verbose" | "input" | "silly" |
|
||||
"emerg" | "alert" | "crit" | "warning" | "notice" |
|
||||
"child"
|
||||
> & {
|
||||
@@ -172,6 +174,7 @@ export type Logger = Omit<winston.Logger,
|
||||
warn: winston.LeveledLogMethod,
|
||||
audit: winston.LeveledLogMethod,
|
||||
info: winston.LeveledLogMethod,
|
||||
http: winston.LeveledLogMethod,
|
||||
server: winston.LeveledLogMethod,
|
||||
verbose: winston.LeveledLogMethod,
|
||||
child(options: object): Logger,
|
||||
|
||||
@@ -163,7 +163,7 @@ export class TerminalFormat {
|
||||
src += `i:${info.instance_name} `;
|
||||
|
||||
} else if (info.host_name) {
|
||||
src += `s:${info.host_name} `;
|
||||
src += `h:${info.host_name} `;
|
||||
}
|
||||
if (info.plugin) {
|
||||
src += `${info.plugin}: `;
|
||||
|
||||
@@ -3,6 +3,54 @@
|
||||
* @module lib/lua_tools
|
||||
*/
|
||||
|
||||
/**
|
||||
* A colour as it may originate from Factorio Lua code, see
|
||||
* https://lua-api.factorio.com/latest/types/Color.html
|
||||
*/
|
||||
export type FactorioColor = { r?: number, g?: number, b?: number, a?: number } | [number, number, number, number?];
|
||||
|
||||
export type Color = { r: number, g: number, b: number, a: number };
|
||||
|
||||
/**
|
||||
* Normalise a color as it may appear in Factorio Lua code to be an object
|
||||
* with r, g, b, a properties in the range 0 to 1.
|
||||
*
|
||||
* @param color -
|
||||
* A colour that may be specified with left out components or as an
|
||||
* array and have the range 1 to 0 or 255 to 0.
|
||||
* @returns
|
||||
* Normalised colour where all 4 components are present as properties in
|
||||
* the range 0 to 1.
|
||||
*/
|
||||
export function normalizeColor(color: FactorioColor) {
|
||||
let r, g, b, a;
|
||||
if (color instanceof Array) {
|
||||
([r, g, b, a] = color);
|
||||
} else {
|
||||
({ r, g, b, a } = color);
|
||||
}
|
||||
|
||||
r ??= NaN;
|
||||
g ??= NaN;
|
||||
b ??= NaN;
|
||||
a ??= NaN;
|
||||
|
||||
// Note NaN > 1 is false and NaN / 255 is NaN
|
||||
if (r > 1 || g > 1 || b > 1 || a > 1) {
|
||||
r /= 255;
|
||||
g /= 255;
|
||||
b /= 255;
|
||||
a /= 255;
|
||||
}
|
||||
|
||||
return {
|
||||
r: Number.isNaN(r) ? 0 : r,
|
||||
g: Number.isNaN(g) ? 0 : g,
|
||||
b: Number.isNaN(b) ? 0 : b,
|
||||
a: Number.isNaN(a) ? 1 : a,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes a string for inclusion into a lua string
|
||||
*
|
||||
|
||||
@@ -239,11 +239,11 @@ definePermission({
|
||||
definePermission({
|
||||
name: "core.instance.save.list",
|
||||
title: "List saves",
|
||||
description: "List the saves currently on the instance.",
|
||||
description: "List the saves currently on instances.",
|
||||
});
|
||||
definePermission({
|
||||
name: "core.instance.save.list.subscribe",
|
||||
title: "Subscribe to save list updatess",
|
||||
name: "core.instance.save.subscribe",
|
||||
title: "Subscribe to save updates",
|
||||
description: "Subscribe to be notifed on updates to the list of saves of instances.",
|
||||
});
|
||||
definePermission({
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Type, Static } from "@sinclair/typebox";
|
||||
import { Link, Event, EventClass, RequestHandler, WebSocketClientConnector, WebSocketBaseConnector } from "./link";
|
||||
import { logger } from "./logging";
|
||||
import { Address, MessageRequest, IControllerUser } from "./data";
|
||||
import isDeepStrictEqual from "./is_deep_strict_equal";
|
||||
|
||||
export type SubscriptionRequestHandler<T> = RequestHandler<SubscriptionRequest, Event<T> | null>;
|
||||
export type EventSubscriberCallback<T> = (updates: T[], synced: boolean) => void
|
||||
@@ -204,6 +205,14 @@ export class EventSubscriber<
|
||||
async _handle(event: EventSubscribable<T, V>) {
|
||||
for (const value of event.updates) {
|
||||
this.lastResponseTimeMs = Math.max(this.lastResponseTimeMs, value.updatedAtMs);
|
||||
// Warn about updates which changes content but don't update updatedAtMs
|
||||
const existing = this.values.get(value.id as K);
|
||||
if (existing && existing.updatedAtMs === value.updatedAtMs && !isDeepStrictEqual(existing, value)) {
|
||||
logger.warn(
|
||||
`${this.Event.name} contains update for value with id ${value.id} that has an identical ` +
|
||||
"updatedAtMs timestamp but differing content"
|
||||
);
|
||||
}
|
||||
if (value.isDeleted) {
|
||||
this.values.delete(value.id as K);
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig.web.json",
|
||||
"extends": "../../tsconfig.browser.json",
|
||||
"include": [
|
||||
"browser.ts",
|
||||
"package.json",
|
||||
@@ -12,6 +12,7 @@
|
||||
"./src/hash.ts",
|
||||
"./src/ini.ts",
|
||||
"./src/link/*.ts",
|
||||
"./src/lua_tools.ts",
|
||||
"./src/logging.ts",
|
||||
"./src/permissions.ts",
|
||||
"./src/prometheus.ts",
|
||||
@@ -25,6 +26,6 @@
|
||||
],
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"outDir": "dist/web/build",
|
||||
"outDir": "dist/browser",
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.node.json",
|
||||
"include": ["index.ts", "browser.ts", "build_mod.js", "src/**/*.ts", "src/**/*.js"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"allowJs": true,
|
||||
},
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.node.json" },
|
||||
{ "path": "./tsconfig.browser.json" },
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.node.json",
|
||||
"include": ["index.ts", "browser.ts", "build_mod.js", "src/**/*.ts", "src/**/*.js"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/node",
|
||||
"allowJs": true,
|
||||
},
|
||||
}
|
||||
@@ -3,9 +3,9 @@
|
||||
"description": "Clusterio web interface implementation",
|
||||
"author": "Hornwitser",
|
||||
"license": "MIT",
|
||||
"version": "2.0.0-alpha.14",
|
||||
"version": "2.0.0-alpha.17",
|
||||
"browser": "src/index.ts",
|
||||
"main": "dist/src/index.js",
|
||||
"main": "dist/browser/src/index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
@@ -17,12 +17,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.1.4",
|
||||
"@babel/core": "^7.22.9",
|
||||
"@babel/preset-react": "^7.22.5",
|
||||
"@clusterio/lib": "^2.0.0-alpha.14",
|
||||
"@clusterio/lib": "^2.0.0-alpha.17",
|
||||
"@swc/core": "^1.4.0",
|
||||
"antd": "^5.13.0",
|
||||
"assert": "^2.0.0",
|
||||
"babel-loader": "^9.1.3",
|
||||
"browserify-zlib": "^0.2.0",
|
||||
"buffer": "^6.0.3",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
@@ -39,8 +37,8 @@
|
||||
"react-router-dom": "^6.14.2",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"style-loader": "^3.3.3",
|
||||
"swc-loader": "^0.2.6",
|
||||
"terser-webpack-plugin": "^5.3.9",
|
||||
"ts-loader": "^9.4.4",
|
||||
"typescript": "^5.1.6",
|
||||
"util": "^0.12.5",
|
||||
"webpack": "^5.88.2",
|
||||
|
||||
@@ -104,16 +104,16 @@ export default function App(props: AppProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary Component={ErrorCard}>
|
||||
<ConfigProvider
|
||||
theme={{ algorithm: theme.darkAlgorithm }}
|
||||
>
|
||||
<ConfigProvider
|
||||
theme={{ algorithm: theme.darkAlgorithm }}
|
||||
>
|
||||
<ErrorBoundary Component={ErrorCard}>
|
||||
<ControlContext.Provider value={props.control}>
|
||||
<BrowserRouter basename={webRoot}>
|
||||
{page}
|
||||
</BrowserRouter>
|
||||
</ControlContext.Provider>
|
||||
</ConfigProvider>
|
||||
</ErrorBoundary>
|
||||
</ErrorBoundary>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
@@ -27,6 +27,8 @@ export default function ControllerConfigTree() {
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
value = undefined;
|
||||
}
|
||||
await control.send(new lib.ControllerConfigSetPropRequest(field, prop, value));
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ export default function HostConfigTree(props: { id: number, available: boolean }
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
value = undefined;
|
||||
}
|
||||
await control.sendTo({ hostId: props.id }, new lib.HostConfigSetPropRequest(field, prop, value));
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function HostViewPage() {
|
||||
|
||||
let hostButtons = <Space>
|
||||
{
|
||||
account.hasPermission("core.host.revoke_access")
|
||||
account.hasPermission("core.host.revoke_token")
|
||||
&& <Popconfirm
|
||||
title={`Revoke tokens of ${host.name}?`}
|
||||
placement="bottomRight"
|
||||
|
||||
@@ -28,6 +28,8 @@ export default function InstanceConfigTree(props: InstanceConfigTreeProps) {
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
value = undefined;
|
||||
}
|
||||
await control.send(new lib.InstanceConfigSetPropRequest(props.id, field, prop, value));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { message, Button, Space, Table } from "antd";
|
||||
import { message, Button, Space, Table, Typography } from "antd";
|
||||
import CopyOutlined from "@ant-design/icons/CopyOutlined";
|
||||
import type { SizeType } from "antd/es/config-provider/SizeContext";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
@@ -10,6 +10,7 @@ import { useHosts } from "../model/host";
|
||||
import InstanceStatusTag from "./InstanceStatusTag";
|
||||
import StartStopInstanceButton from "./StartStopInstanceButton";
|
||||
import { InstanceDetails } from "@clusterio/lib";
|
||||
import Link from "./Link";
|
||||
|
||||
const strcmp = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }).compare;
|
||||
|
||||
@@ -55,7 +56,12 @@ export default function InstanceList(props: InstanceListProps) {
|
||||
{
|
||||
title: "Assigned Host",
|
||||
key: "assignedHost",
|
||||
render: (_, instance) => hostName(instance.assignedHost),
|
||||
render: (_, instance) => <Link
|
||||
to={`/hosts/${instance.assignedHost}/view`}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
{hostName(instance.assignedHost)}
|
||||
</Link>,
|
||||
sorter: (a, b) => strcmp(hostName(a.assignedHost), hostName(b.assignedHost)),
|
||||
responsive: ["sm"],
|
||||
},
|
||||
@@ -68,10 +74,10 @@ export default function InstanceList(props: InstanceListProps) {
|
||||
{publicAddress}
|
||||
<Button
|
||||
type="text"
|
||||
icon={<CopyOutlined/>}
|
||||
icon={<CopyOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigator.clipboard.writeText(publicAddress??"");
|
||||
navigator.clipboard.writeText(publicAddress ?? "");
|
||||
message.success("Copied public address!");
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -23,6 +23,7 @@ import { notifyErrorHandler } from "../util/notify";
|
||||
import { useInstance } from "../model/instance";
|
||||
import { useHost } from "../model/host";
|
||||
import InstanceStatusTag from "./InstanceStatusTag";
|
||||
import Link from "./Link";
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
@@ -42,7 +43,9 @@ function InstanceDescription(props: InstanceDescriptionProps) {
|
||||
<Descriptions.Item label="Host">
|
||||
{!assigned
|
||||
? <em>Unassigned</em>
|
||||
: host?.name ?? instance.assignedHost
|
||||
: <Link to={`/hosts/${host?.id ?? instance.assignedHost}/view`}>
|
||||
{host?.name ?? instance.assignedHost}
|
||||
</Link>
|
||||
}
|
||||
{account.hasPermission("core.instance.assign") && <AssignInstanceModal
|
||||
id={instance.id}
|
||||
@@ -104,7 +107,7 @@ function InstanceButtons(props: { instance: lib.InstanceDetails }) {
|
||||
}
|
||||
let instanceButtonsMenuProps: MenuProps = {
|
||||
items: instanceButtonMenuItems,
|
||||
onClick: ({ key }: { key:string }) => {
|
||||
onClick: ({ key }: { key: string }) => {
|
||||
if (key === "export") {
|
||||
setExportingData(true);
|
||||
control.sendTo(
|
||||
@@ -156,9 +159,10 @@ function InstanceButtons(props: { instance: lib.InstanceDetails }) {
|
||||
"core.instance.extract_players",
|
||||
"core.instance.kill",
|
||||
"core.instance.delete",
|
||||
) && <Dropdown placement="bottomRight" trigger={["click"]} menu={instanceButtonsMenuProps}>
|
||||
<Button>More <DownOutlined /></Button>
|
||||
</Dropdown>}
|
||||
)
|
||||
&& <Dropdown placement="bottomRight" trigger={["click"]} menu={instanceButtonsMenuProps}>
|
||||
<Button>More <DownOutlined /></Button>
|
||||
</Dropdown>}
|
||||
</Space>;
|
||||
}
|
||||
|
||||
@@ -180,7 +184,7 @@ export default function InstanceViewPage() {
|
||||
|
||||
return <PageLayout nav={nav}>
|
||||
<Alert
|
||||
message={"Instance not found" }
|
||||
message={"Instance not found"}
|
||||
showIcon
|
||||
description={<>Instance with id {instanceId} was not found on the controller.</>}
|
||||
type="warning"
|
||||
@@ -198,13 +202,13 @@ export default function InstanceViewPage() {
|
||||
|
||||
return <PageLayout nav={nav}>
|
||||
<PageHeader
|
||||
title={instance.name??""}
|
||||
extra={<InstanceButtons instance={instance}/>}
|
||||
title={instance.name ?? ""}
|
||||
extra={<InstanceButtons instance={instance} />}
|
||||
/>
|
||||
<InstanceDescription host={host} instance={instance} />
|
||||
|
||||
{
|
||||
account.hasAllPermission("core.instance.save.list", "core.instance.save.list_subscribe")
|
||||
account.hasAllPermission("core.instance.save.list", "core.instance.save.subscribe")
|
||||
&& <SavesList instance={instance} />
|
||||
}
|
||||
{
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from "react";
|
||||
import { Typography } from "antd";
|
||||
import {
|
||||
LinkProps,
|
||||
useHref,
|
||||
useLinkClickHandler,
|
||||
} from "react-router-dom";
|
||||
import { BaseType } from "antd/es/typography/Base";
|
||||
|
||||
const Link = React.forwardRef(
|
||||
(
|
||||
{
|
||||
onClick,
|
||||
replace = false,
|
||||
state,
|
||||
target,
|
||||
to,
|
||||
type,
|
||||
...rest
|
||||
}: LinkProps,
|
||||
ref
|
||||
) => {
|
||||
let href = useHref(to);
|
||||
let handleClick = useLinkClickHandler(to, {
|
||||
replace,
|
||||
state,
|
||||
target,
|
||||
}) as (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
||||
|
||||
return (
|
||||
<Typography.Link
|
||||
{...rest}
|
||||
href={href}
|
||||
onClick={(event) => {
|
||||
onClick?.(event as React.MouseEvent<HTMLAnchorElement, MouseEvent>);
|
||||
if (!event.defaultPrevented) {
|
||||
handleClick(event);
|
||||
}
|
||||
}}
|
||||
ref={ref as React.RefObject<HTMLElement> | null}
|
||||
target={target}
|
||||
type={type as BaseType}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
export default Link;
|
||||
@@ -202,7 +202,7 @@ function SearchModsTable(props: SearchModsTableProps) {
|
||||
align: "right",
|
||||
render: (_, result) => <Select
|
||||
size="small"
|
||||
bordered={false}
|
||||
variant="borderless"
|
||||
value={modResultSelectedVersion.get(result.name) || 0}
|
||||
onChange={index => {
|
||||
const newVersions = new Map(modResultSelectedVersion);
|
||||
@@ -801,7 +801,7 @@ export default function ModPackViewPage() {
|
||||
title={modPack.name}
|
||||
extra={<Space>
|
||||
<ExportButton modPack={modifiedModPack}/>
|
||||
{account.hasPermission("core.mod-pack.delete") && <Popconfirm
|
||||
{account.hasPermission("core.mod_pack.delete") && <Popconfirm
|
||||
title="Delete mod pack and all of its settings?"
|
||||
placement="bottomRight"
|
||||
okText="Delete"
|
||||
|
||||
@@ -186,8 +186,8 @@ export default function ModsPage() {
|
||||
<SectionHeader
|
||||
title="Mod Packs"
|
||||
extra={<Space>
|
||||
{account.hasPermission("core.mod-pack.create") && <ImportModPackButton />}
|
||||
{account.hasPermission("core.mod-pack.create") && <CreateModPackButton />}
|
||||
{account.hasPermission("core.mod_pack.create") && <ImportModPackButton />}
|
||||
{account.hasPermission("core.mod_pack.create") && <CreateModPackButton />}
|
||||
</Space>}
|
||||
/>
|
||||
<Table
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Layout, Breadcrumb } from "antd";
|
||||
import Link from "./Link";
|
||||
|
||||
const { Content } = Layout;
|
||||
|
||||
|
||||
@@ -207,6 +207,7 @@ export default function SavesList(props: { instance: lib.InstanceDetails }) {
|
||||
|
||||
let hostOffline = ["unassigned", "unknown"].includes(props.instance.status!);
|
||||
const saveTable = <Table
|
||||
className="save-list-table"
|
||||
size="small"
|
||||
columns={[
|
||||
{
|
||||
@@ -236,7 +237,6 @@ export default function SavesList(props: { instance: lib.InstanceDetails }) {
|
||||
]}
|
||||
dataSource={[...saves.values()]}
|
||||
rowKey={save => save.name}
|
||||
pagination={false}
|
||||
expandable={{
|
||||
columnWidth: 33,
|
||||
expandRowByClick: true,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import React, { Fragment, useContext, useEffect, useState } from "react";
|
||||
import { Route, Routes, useNavigate } from "react-router-dom";
|
||||
import { Dropdown, Layout, Menu, MenuProps } from "antd";
|
||||
import UserOutlined from "@ant-design/icons/UserOutlined";
|
||||
@@ -115,12 +115,12 @@ export default function SiteLayout() {
|
||||
{combinedPages.map(({path, sidebarPath, content}) => <Route
|
||||
path={path}
|
||||
key={path}
|
||||
element={<>
|
||||
element={<Fragment key={path}>
|
||||
<SetSidebar path={sidebarPath ? sidebarPath : path} />
|
||||
<ErrorBoundary Component={ErrorPage}>
|
||||
{content}
|
||||
</ErrorBoundary>
|
||||
</>}
|
||||
</Fragment>}
|
||||
/>)}
|
||||
<Route element={<SetSidebar path={null} />} />
|
||||
</Routes>
|
||||
|
||||
@@ -88,7 +88,6 @@ export default function UserViewPage() {
|
||||
onChange={() => setRolesDirty(true)}
|
||||
loading={!roles}
|
||||
mode="multiple"
|
||||
showArrow={true}
|
||||
filterOption={(inputValue:string, option: any): boolean => {
|
||||
let role = roles.get(option.value);
|
||||
return role?.name.toLowerCase().includes(inputValue.toLowerCase()) ?? false;
|
||||
|
||||
@@ -12,6 +12,7 @@ import PageHeader from "./PageHeader";
|
||||
import PageLayout from "./PageLayout";
|
||||
import PluginExtra from "./PluginExtra";
|
||||
import { formatFirstSeen, formatLastSeen, sortFirstSeen, sortLastSeen, useUsers } from "../model/user";
|
||||
import Link from "./Link";
|
||||
|
||||
const strcmp = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }).compare;
|
||||
|
||||
@@ -49,7 +50,7 @@ function CreateUserButton() {
|
||||
>
|
||||
<Form form={form}>
|
||||
<Form.Item name="userName" label="Name">
|
||||
<Input/>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
@@ -97,7 +98,9 @@ export default function UsersPage() {
|
||||
title: "Roles",
|
||||
key: "roles",
|
||||
render: (_, user) => (
|
||||
[...user.roleIds].map(id => <Tag key={id}>{(roles.get(id) || { name: id }).name}</Tag>)
|
||||
[...user.roleIds].map(id => <Link to={`/roles/${id}/view`} onClick={e => e.stopPropagation()}>
|
||||
<Tag key={id}>{(roles.get(id) || { name: id }).name}</Tag>
|
||||
</Link>)
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -28,3 +28,5 @@ export { default as PageHeader } from "./components/PageHeader";
|
||||
export { default as PageLayout } from "./components/PageLayout";
|
||||
export { default as SavesList } from "./components/SavesList";
|
||||
export { default as SectionHeader } from "./components/SectionHeader";
|
||||
|
||||
export { default as Link } from "./components/Link";
|
||||
@@ -22,6 +22,12 @@ export function useAccount(): UserAccount {
|
||||
};
|
||||
}, [control]);
|
||||
|
||||
function checkPermissionExists(permission: string) {
|
||||
if (!lib.permissions.has(permission)) {
|
||||
throw new Error(`permission ${permission} does not exist`);
|
||||
}
|
||||
}
|
||||
|
||||
function hasPermission(permission: string): boolean {
|
||||
for (let role of roles) {
|
||||
if (role.permissions.includes("core.admin") || role.permissions.includes(permission)) {
|
||||
@@ -38,12 +44,16 @@ export function useAccount(): UserAccount {
|
||||
if (!roles) {
|
||||
return null;
|
||||
}
|
||||
checkPermissionExists(permission);
|
||||
return hasPermission(permission);
|
||||
},
|
||||
hasAnyPermission(...permissions: string[]): boolean | null {
|
||||
if (!roles) {
|
||||
return null;
|
||||
}
|
||||
for (let permission of permissions) {
|
||||
checkPermissionExists(permission);
|
||||
}
|
||||
for (let permission of permissions) {
|
||||
if (hasPermission(permission)) {
|
||||
return true;
|
||||
@@ -55,6 +65,9 @@ export function useAccount(): UserAccount {
|
||||
if (!roles) {
|
||||
return null;
|
||||
}
|
||||
for (let permission of permissions) {
|
||||
checkPermissionExists(permission);
|
||||
}
|
||||
for (let permission of permissions) {
|
||||
if (!hasPermission(permission)) {
|
||||
return false;
|
||||
|
||||
@@ -47,7 +47,7 @@ export const pages: PluginPage[] = [
|
||||
{
|
||||
path: "/mods",
|
||||
sidebarName: "Mods",
|
||||
permission: (account: UserAccount) => account.hasAnyPermission("core.mod.list", "core.mod-pack.list"),
|
||||
permission: (account: UserAccount) => account.hasAnyPermission("core.mod.list", "core.mod_pack.list"),
|
||||
content: <ModsPage />,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -182,10 +182,19 @@ body {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.save-list-dragger .ant-upload.ant-upload-drag {
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.save-list-dragger .ant-upload.ant-upload-btn {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.save-list-table .ant-table-pagination.ant-pagination {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.rcon-result code {
|
||||
display: block;
|
||||
background: #000;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.web.json",
|
||||
"extends": "../../tsconfig.browser.json",
|
||||
"references": [
|
||||
{ "path": "../lib/tsconfig.web.json" },
|
||||
{ "path": "../lib/tsconfig.browser.json" },
|
||||
],
|
||||
"include": [ "src/**/*.tsx", "src/**/*.ts", "package.json", "global.d.ts" ],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"outDir": "dist/browser",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.browser.json" },
|
||||
],
|
||||
}
|
||||
@@ -65,32 +65,47 @@ module.exports = (env = {}) => ({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
resolve: {
|
||||
// XXX Required due to bug in babel-runtime
|
||||
// see: https://github.com/babel/babel/issues/12058
|
||||
fullySpecified: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: require.resolve("ts-loader"),
|
||||
loader: require.resolve("swc-loader"),
|
||||
options: {
|
||||
configFile: "tsconfig.web.json",
|
||||
projectReferences: true,
|
||||
sourceMaps: true,
|
||||
jsc: {
|
||||
parser: {
|
||||
syntax: "typescript",
|
||||
tsx: true,
|
||||
},
|
||||
target: "es2022",
|
||||
transform: {
|
||||
react: {
|
||||
runtime: "automatic",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.jsx$/,
|
||||
test: /\.jsx?$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: require.resolve("babel-loader"),
|
||||
loader: require.resolve("swc-loader"),
|
||||
options: {
|
||||
presets: [require.resolve("@babel/preset-react")],
|
||||
sourceMaps: true,
|
||||
jsc: {
|
||||
parser: {
|
||||
syntax: "ecmascript",
|
||||
jsx: true,
|
||||
},
|
||||
target: "es2022",
|
||||
transform: {
|
||||
react: {
|
||||
runtime: "automatic",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -123,10 +138,10 @@ module.exports = (env = {}) => ({
|
||||
moduleIds: "deterministic",
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
minify: TerserPlugin.swcMinify,
|
||||
terserOptions: {
|
||||
compress: {
|
||||
keep_classnames: true,
|
||||
passes: 2,
|
||||
},
|
||||
mangle: {
|
||||
keep_classnames: true,
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
webpack.config.js
|
||||
dist/web/build
|
||||
dist/browser
|
||||
@@ -6,8 +6,8 @@ export const plugin: lib.PluginDeclaration = {
|
||||
name: "global_chat",
|
||||
title: "Global Chat",
|
||||
description: "Forwards chat between instances.",
|
||||
instanceEntrypoint: "dist/plugin/instance",
|
||||
ctlEntrypoint: "dist/plugin/control",
|
||||
instanceEntrypoint: "dist/node/instance",
|
||||
ctlEntrypoint: "dist/node/control",
|
||||
|
||||
messages: [
|
||||
ChatEvent,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@clusterio/plugin-global_chat",
|
||||
"version": "2.0.0-alpha.14",
|
||||
"version": "2.0.0-alpha.17",
|
||||
"description": "Clusterio plugin forwarding between Factorio servers",
|
||||
"author": "Hornwitser <github@hornwitser.no>",
|
||||
"homepage": "https://github.com/clusterio/clusterio#readme",
|
||||
"license": "MIT",
|
||||
"repository": "clusterio/clusterio",
|
||||
"main": "dist/plugin/index.js",
|
||||
"main": "dist/node/index.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build && webpack-cli --env production",
|
||||
"test": "echo \"Error: run tests from root\" && exit 1"
|
||||
@@ -21,8 +21,8 @@
|
||||
"@clusterio/lib": "^2.0.0-alpha.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@clusterio/lib": "^2.0.0-alpha.14",
|
||||
"@clusterio/web_ui": "^2.0.0-alpha.14",
|
||||
"@clusterio/lib": "^2.0.0-alpha.17",
|
||||
"@clusterio/web_ui": "^2.0.0-alpha.17",
|
||||
"@types/node": "^20.4.5",
|
||||
"@types/react": "^18.2.21",
|
||||
"typescript": "^5.1.6",
|
||||
|
||||
@@ -4,9 +4,9 @@ const lib = require("@clusterio/lib");
|
||||
|
||||
const mock = require("../../../test/mock");
|
||||
const lines = require("../../../test/lib/factorio/lines");
|
||||
const instance = require("../dist/plugin/instance");
|
||||
const info = require("../dist/plugin/index").plugin;
|
||||
const { ChatEvent } = require("../dist/plugin/messages");
|
||||
const instance = require("../dist/node/instance");
|
||||
const info = require("../dist/node/index").plugin;
|
||||
const { ChatEvent } = require("../dist/node/messages");
|
||||
|
||||
|
||||
describe("global_chat plugin", function() {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.browser.json",
|
||||
"references": [
|
||||
{ "path": "../../packages/lib/tsconfig.browser.json" },
|
||||
{ "path": "../../packages/web_ui/tsconfig.browser.json" },
|
||||
],
|
||||
"include": [ "web/**/*.tsx", "web/**/*.ts", "messages.ts", "package.json" ],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/browser",
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.node.json",
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "../../packages/lib" },
|
||||
{ "path": "./tsconfig.node.json" },
|
||||
{ "path": "./tsconfig.browser.json" },
|
||||
],
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": ["test/*", "./dist/*"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/plugin",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.node.json",
|
||||
"references": [
|
||||
{ "path": "../../packages/lib/tsconfig.node.json" },
|
||||
],
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": ["test/*", "./dist/*"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/node",
|
||||
},
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.web.json",
|
||||
"references": [
|
||||
{ "path": "../../packages/lib/tsconfig.web.json" },
|
||||
{ "path": "../../packages/web_ui/tsconfig.web.json" },
|
||||
],
|
||||
"include": [ "web/**/*.tsx", "web/**/*.ts", "messages.ts", "package.json" ],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/web/build",
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
webpack.config.js
|
||||
dist/web/build
|
||||
dist/browser
|
||||
@@ -190,7 +190,7 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
}))
|
||||
.sort((a, b) => b.length - a.length);
|
||||
return new msg.DatabaseStatsRequest.Response(
|
||||
playerDatastore.map(x => x.length).reduce((a, b) => b - a, 0),
|
||||
playerDatastore.map(x => x.length).reduce((acc, val) => acc + val, 0),
|
||||
playerDatastore.length,
|
||||
{
|
||||
name: playerDatastore[0] && playerDatastore[0].name || "-",
|
||||
|
||||
@@ -22,7 +22,7 @@ export const plugin: lib.PluginDeclaration = {
|
||||
title: "Inventory sync",
|
||||
description: "Synchronizes players inventories between instances",
|
||||
|
||||
instanceEntrypoint: "dist/plugin/instance",
|
||||
instanceEntrypoint: "dist/node/instance",
|
||||
instanceConfigFields: {
|
||||
"inventory_sync.rcon_chunk_size": {
|
||||
title: "Rcon inventory chunk size",
|
||||
@@ -33,7 +33,7 @@ export const plugin: lib.PluginDeclaration = {
|
||||
},
|
||||
},
|
||||
|
||||
controllerEntrypoint: "dist/plugin/controller",
|
||||
controllerEntrypoint: "dist/node/controller",
|
||||
controllerConfigFields: {
|
||||
"inventory_sync.player_lock_timeout": {
|
||||
title: "Player Lock Timeout",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@clusterio/plugin-inventory_sync",
|
||||
"version": "2.0.0-alpha.14",
|
||||
"version": "2.0.0-alpha.17",
|
||||
"description": "Clusterio plugin forwarding between Factorio servers",
|
||||
"author": "Danielv <danielv@danielv.no>",
|
||||
"homepage": "https://github.com/clusterio/factorioClusterio#readme",
|
||||
"license": "MIT",
|
||||
"repository": "clusterio/factorioClusterio",
|
||||
"main": "dist/plugin/index.js",
|
||||
"main": "dist/node/index.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build && webpack-cli --env production",
|
||||
"test": "echo \"Error: run tests from root\" && exit 1"
|
||||
@@ -25,8 +25,8 @@
|
||||
"fs-extra": "^8.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@clusterio/lib": "^2.0.0-alpha.14",
|
||||
"@clusterio/web_ui": "^2.0.0-alpha.14",
|
||||
"@clusterio/lib": "^2.0.0-alpha.17",
|
||||
"@clusterio/web_ui": "^2.0.0-alpha.17",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/node": "^20.4.5",
|
||||
"@types/react": "^18.2.21",
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.browser.json",
|
||||
"references": [
|
||||
{ "path": "../../packages/lib/tsconfig.browser.json" },
|
||||
{ "path": "../../packages/web_ui/tsconfig.browser.json" },
|
||||
],
|
||||
"include": [ "web/**/*.tsx", "web/**/*.ts", "messages.ts", "package.json" ],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/browser",
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.node.json",
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "../../packages/lib" },
|
||||
{ "path": "./tsconfig.node.json" },
|
||||
{ "path": "./tsconfig.browser.json" },
|
||||
],
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": ["test/*", "./dist/*"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/plugin",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.node.json",
|
||||
"references": [
|
||||
{ "path": "../../packages/lib/tsconfig.node.json" },
|
||||
],
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": ["test/*", "./dist/*"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/node",
|
||||
},
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.web.json",
|
||||
"references": [
|
||||
{ "path": "../../packages/lib/tsconfig.web.json" },
|
||||
{ "path": "../../packages/web_ui/tsconfig.web.json" },
|
||||
],
|
||||
"include": [ "web/**/*.tsx", "web/**/*.ts", "messages.ts", "package.json" ],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/web/build",
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
webpack.config.js
|
||||
dist/web/build
|
||||
dist/browser
|
||||
@@ -15,8 +15,8 @@ export const plugin: lib.PluginDeclaration = {
|
||||
name: "player_auth",
|
||||
title: "Player Auth",
|
||||
description: "Provides authentication to the cluster via logging into a Factorio server.",
|
||||
controllerEntrypoint: "dist/plugin/controller",
|
||||
instanceEntrypoint: "dist/plugin/instance",
|
||||
controllerEntrypoint: "dist/node/controller",
|
||||
instanceEntrypoint: "dist/node/instance",
|
||||
webEntrypoint: "./web",
|
||||
controllerConfigFields: {
|
||||
"player_auth.code_length": {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@clusterio/plugin-player_auth",
|
||||
"version": "2.0.0-alpha.14",
|
||||
"version": "2.0.0-alpha.17",
|
||||
"description": "Clusterio plugin authenticating logged in players to the web interface",
|
||||
"author": "Hornwitser <github@hornwitser.no>",
|
||||
"homepage": "https://github.com/clusterio/clusterio#readme",
|
||||
"license": "MIT",
|
||||
"repository": "clusterio/clusterio",
|
||||
"main": "dist/plugin/index.js",
|
||||
"main": "dist/node/index.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build && webpack-cli --env production",
|
||||
"test": "echo \"Error: run tests from root\" && exit 1"
|
||||
@@ -26,8 +26,8 @@
|
||||
"jsonwebtoken": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@clusterio/lib": "^2.0.0-alpha.14",
|
||||
"@clusterio/web_ui": "^2.0.0-alpha.14",
|
||||
"@clusterio/lib": "^2.0.0-alpha.17",
|
||||
"@clusterio/web_ui": "^2.0.0-alpha.17",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jsonwebtoken": "^9.0.2",
|
||||
"@types/node": "^20.4.5",
|
||||
|
||||
@@ -5,10 +5,10 @@ const jwt = require("jsonwebtoken");
|
||||
|
||||
const mock = require("../../../test/mock");
|
||||
|
||||
const controller = require("../dist/plugin/controller");
|
||||
const instance = require("../dist/plugin/instance");
|
||||
const info = require("../dist/plugin/index").plugin;
|
||||
const { FetchPlayerCodeRequest, SetVerifyCodeRequest } = require("../dist/plugin/messages");
|
||||
const controller = require("../dist/node/controller");
|
||||
const instance = require("../dist/node/instance");
|
||||
const info = require("../dist/node/index").plugin;
|
||||
const { FetchPlayerCodeRequest, SetVerifyCodeRequest } = require("../dist/node/messages");
|
||||
const lib = require("@clusterio/lib");
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.browser.json",
|
||||
"references": [
|
||||
{ "path": "../../packages/lib/tsconfig.browser.json" },
|
||||
{ "path": "../../packages/web_ui/tsconfig.browser.json" },
|
||||
],
|
||||
"include": [ "web/**/*.tsx", "web/**/*.ts", "messages.ts", "package.json" ],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/browser",
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.node.json",
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "../../packages/lib" },
|
||||
{ "path": "./tsconfig.node.json" },
|
||||
{ "path": "./tsconfig.browser.json" },
|
||||
],
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": ["test/*", "./dist/*"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/plugin",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.node.json",
|
||||
"references": [
|
||||
{ "path": "../../packages/lib/tsconfig.node.json" },
|
||||
],
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": ["test/*", "./dist/*"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/node",
|
||||
},
|
||||
}
|
||||
Loaded 100 of 147 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user