diff --git a/.eslintrc.js b/.eslintrc.js index eefb4bc1..659219fd 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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"], }, }], }, diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..f301fedf --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +auto-install-peers=false diff --git a/CHANGELOG.md b/CHANGELOG.md index ca5d4e2f..a15e75b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,86 @@ # Changelog + + +## 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 diff --git a/README.md b/README.md index 10be24e6..c00b5ab5 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..8f46d2de --- /dev/null +++ b/codecov.yml @@ -0,0 +1,5 @@ +coverage: + status: + project: + default: + if_ci_failed: success diff --git a/docs/contributing.md b/docs/contributing.md index 44582f4a..9a4ef6a9 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -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 ``` diff --git a/docs/devs/plugin-migration-to-alpha-14.md b/docs/devs/plugin-migration-to-alpha-14.md index 91962795..28266664 100644 --- a/docs/devs/plugin-migration-to-alpha-14.md +++ b/docs/devs/plugin-migration-to-alpha-14.md @@ -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`: diff --git a/docs/writing-plugins.md b/docs/writing-plugins.md index ead6f6e5..dd9e97a6 100644 --- a/docs/writing-plugins.md +++ b/docs/writing-plugins.md @@ -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" })); } ``` diff --git a/package.json b/package.json index 336edd58..a5f4a506 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/controller/.npmignore b/packages/controller/.npmignore index ebd99907..55622581 100644 --- a/packages/controller/.npmignore +++ b/packages/controller/.npmignore @@ -1,2 +1,2 @@ webpack.config.js -dist/web/build +dist/browser diff --git a/packages/controller/controller.ts b/packages/controller/controller.ts index 14b18e46..b4161f65 100644 --- a/packages/controller/controller.ts +++ b/packages/controller/controller.ts @@ -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", diff --git a/packages/controller/index.ts b/packages/controller/index.ts index 5218034f..caebb912 100644 --- a/packages/controller/index.ts +++ b/packages/controller/index.ts @@ -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"; diff --git a/packages/controller/package.json b/packages/controller/package.json index 6f967415..61a728e4 100644 --- a/packages/controller/package.json +++ b/packages/controller/package.json @@ -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", diff --git a/packages/controller/src/BaseConnection.ts b/packages/controller/src/BaseConnection.ts index 493ebbde..3ebac51e 100644 --- a/packages/controller/src/BaseConnection.ts +++ b/packages/controller/src/BaseConnection.ts @@ -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); } diff --git a/packages/controller/src/Controller.ts b/packages/controller/src/Controller.ts index 8930f854..e1e53293 100644 --- a/packages/controller/src/Controller.ts +++ b/packages/controller/src/Controller.ts @@ -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); } diff --git a/packages/controller/src/ControllerRouter.ts b/packages/controller/src/ControllerRouter.ts index 84420595..4072c941 100644 --- a/packages/controller/src/ControllerRouter.ts +++ b/packages/controller/src/ControllerRouter.ts @@ -7,8 +7,6 @@ import ControlConnection from "./ControlConnection"; const { logger } = lib; export default class ControllerRouter { - pendingRequests: Map = 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; diff --git a/packages/controller/src/HostConnection.ts b/packages/controller/src/HostConnection.ts index 564b5b96..ab566a40 100644 --- a/packages/controller/src/HostConnection.ts +++ b/packages/controller/src/HostConnection.ts @@ -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; } diff --git a/packages/controller/src/UserManager.ts b/packages/controller/src/UserManager.ts index bb3cde0a..e9239428 100644 --- a/packages/controller/src/UserManager.ts +++ b/packages/controller/src/UserManager.ts @@ -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; + } } diff --git a/packages/controller/tsconfig.browser.json b/packages/controller/tsconfig.browser.json new file mode 100644 index 00000000..154bd176 --- /dev/null +++ b/packages/controller/tsconfig.browser.json @@ -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", + } +} diff --git a/packages/controller/tsconfig.json b/packages/controller/tsconfig.json index 335b8b16..e074e85f 100644 --- a/packages/controller/tsconfig.json +++ b/packages/controller/tsconfig.json @@ -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", - }, } diff --git a/packages/controller/tsconfig.node.json b/packages/controller/tsconfig.node.json new file mode 100644 index 00000000..9dcc91db --- /dev/null +++ b/packages/controller/tsconfig.node.json @@ -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", + }, +} diff --git a/packages/controller/tsconfig.web.json b/packages/controller/tsconfig.web.json deleted file mode 100644 index 6685a163..00000000 --- a/packages/controller/tsconfig.web.json +++ /dev/null @@ -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", - } -} diff --git a/packages/create/package.json b/packages/create/package.json index 6f320a27..7929cbb1 100644 --- a/packages/create/package.json +++ b/packages/create/package.json @@ -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" diff --git a/packages/ctl/package.json b/packages/ctl/package.json index 18c95a2e..879afe37 100644 --- a/packages/ctl/package.json +++ b/packages/ctl/package.json @@ -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", diff --git a/packages/ctl/tsconfig.json b/packages/ctl/tsconfig.json index 574dad9d..20b10c53 100644 --- a/packages/ctl/tsconfig.json +++ b/packages/ctl/tsconfig.json @@ -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", - }, } diff --git a/packages/ctl/tsconfig.node.json b/packages/ctl/tsconfig.node.json new file mode 100644 index 00000000..3a0fb105 --- /dev/null +++ b/packages/ctl/tsconfig.node.json @@ -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", + }, +} diff --git a/packages/host/host.ts b/packages/host/host.ts index 46827d85..fa195675 100755 --- a/packages/host/host.ts +++ b/packages/host/host.ts @@ -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(); diff --git a/packages/host/index.ts b/packages/host/index.ts index 2b01bc21..f39ce536 100644 --- a/packages/host/index.ts +++ b/packages/host/index.ts @@ -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(); diff --git a/packages/host/package.json b/packages/host/package.json index 54e963e2..b339d0a0 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -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", diff --git a/packages/host/src/Host.ts b/packages/host/src/Host.ts index 7381ad36..75e80ffa 100644 --- a/packages/host/src/Host.ts +++ b/packages/host/src/Host.ts @@ -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}`, diff --git a/packages/host/src/Instance.ts b/packages/host/src/Instance.ts index c4e3d473..f6163611 100644 --- a/packages/host/src/Instance.ts +++ b/packages/host/src/Instance.ts @@ -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(); } } diff --git a/packages/host/src/export.ts b/packages/host/src/export.ts index dcd2b01a..530f8fc9 100644 --- a/packages/host/src/export.ts +++ b/packages/host/src/export.ts @@ -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) ; } diff --git a/packages/host/src/server.ts b/packages/host/src/server.ts index 2510903e..b2476ca8 100644 --- a/packages/host/src/server.ts +++ b/packages/host/src/server.ts @@ -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"; diff --git a/packages/host/tsconfig.json b/packages/host/tsconfig.json index bc67208a..20b10c53 100644 --- a/packages/host/tsconfig.json +++ b/packages/host/tsconfig.json @@ -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", - }, } diff --git a/packages/host/tsconfig.node.json b/packages/host/tsconfig.node.json new file mode 100644 index 00000000..e7a488be --- /dev/null +++ b/packages/host/tsconfig.node.json @@ -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", + }, +} diff --git a/packages/lib/.npmignore b/packages/lib/.npmignore index 5dfa68f1..55239636 100644 --- a/packages/lib/.npmignore +++ b/packages/lib/.npmignore @@ -1 +1 @@ -dist/web/build +dist/browser diff --git a/packages/lib/browser.ts b/packages/lib/browser.ts index 5c430809..97525776 100644 --- a/packages/lib/browser.ts +++ b/packages/lib/browser.ts @@ -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"; diff --git a/packages/lib/package.json b/packages/lib/package.json index 6cd1aebc..82afa633 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -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" diff --git a/packages/lib/scripts/compile_validator.js b/packages/lib/scripts/compile_validator.js index e2eafeb3..9dd5fabd 100644 --- a/packages/lib/scripts/compile_validator.js +++ b/packages/lib/scripts/compile_validator.js @@ -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"); diff --git a/packages/lib/src/config/classes.ts b/packages/lib/src/config/classes.ts index 3e26b2c8..dae4fc8a 100644 --- a/packages/lib/src/config/classes.ts +++ b/packages/lib/src/config/classes.ts @@ -459,7 +459,7 @@ export class Config< setProp( name: Field, prop: string, - value: unknown, + value?: unknown, location = this.location, ) { let def = this.constructor.fieldDefinitions[name]; diff --git a/packages/lib/src/data/ModPack.ts b/packages/lib/src/data/ModPack.ts index 094addb4..85dba1fb 100644 --- a/packages/lib/src/data/ModPack.ts +++ b/packages/lib/src/data/ModPack.ts @@ -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 }); } } } diff --git a/packages/lib/src/data/Role.ts b/packages/lib/src/data/Role.ts index 6356def8..56f104bc 100644 --- a/packages/lib/src/data/Role.ts +++ b/packages/lib/src/data/Role.ts @@ -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) { return new this( json.id, json.name, json.description, - new Set(json.permissions), + new Set(json.permissions.map(this.remapPermission)), ); } diff --git a/packages/lib/src/data/messages_core.ts b/packages/lib/src/data/messages_core.ts index 67ea64a5..b4b3f91e 100644 --- a/packages/lib/src/data/messages_core.ts +++ b/packages/lib/src/data/messages_core.ts @@ -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}]`; diff --git a/packages/lib/src/data/messages_instance.ts b/packages/lib/src/data/messages_instance.ts index ff29d886..d14894d4 100644 --- a/packages/lib/src/data/messages_instance.ts +++ b/packages/lib/src/data/messages_instance.ts @@ -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) { @@ -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; diff --git a/packages/lib/src/link/connectors.ts b/packages/lib/src/link/connectors.ts index 2bc15f19..999800ba 100644 --- a/packages/lib/src/link/connectors.ts +++ b/packages/lib/src/link/connectors.ts @@ -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( request: Request, @@ -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 */ diff --git a/packages/lib/src/link/link.ts b/packages/lib/src/link/link.ts index deabd028..1d3d3381 100644 --- a/packages/lib/src/link/link.ts +++ b/packages/lib/src/link/link.ts @@ -72,7 +72,12 @@ export type RequestHandler = (request: Req, src: libData.Address, dst: export type EventHandler = (event: T, src: libData.Address, dst: libData.Address) => Promise; 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(event: Event, dst: libData.Address) { diff --git a/packages/lib/src/logging.ts b/packages/lib/src/logging.ts index c673f98e..38b66278 100644 --- a/packages/lib/src/logging.ts +++ b/packages/lib/src/logging.ts @@ -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 & { @@ -172,6 +174,7 @@ export type Logger = Omit 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 * diff --git a/packages/lib/src/permissions.ts b/packages/lib/src/permissions.ts index cbaf9a93..306047de 100644 --- a/packages/lib/src/permissions.ts +++ b/packages/lib/src/permissions.ts @@ -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({ diff --git a/packages/lib/src/subscriptions.ts b/packages/lib/src/subscriptions.ts index de155090..120cf2fe 100644 --- a/packages/lib/src/subscriptions.ts +++ b/packages/lib/src/subscriptions.ts @@ -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 = RequestHandler | null>; export type EventSubscriberCallback = (updates: T[], synced: boolean) => void @@ -204,6 +205,14 @@ export class EventSubscriber< async _handle(event: EventSubscribable) { 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 { diff --git a/packages/lib/tsconfig.web.json b/packages/lib/tsconfig.browser.json similarity index 85% rename from packages/lib/tsconfig.web.json rename to packages/lib/tsconfig.browser.json index a4349659..e3c20c4e 100644 --- a/packages/lib/tsconfig.web.json +++ b/packages/lib/tsconfig.browser.json @@ -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", } } diff --git a/packages/lib/tsconfig.json b/packages/lib/tsconfig.json index dbc005eb..e074e85f 100644 --- a/packages/lib/tsconfig.json +++ b/packages/lib/tsconfig.json @@ -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" }, + ], } diff --git a/packages/lib/tsconfig.node.json b/packages/lib/tsconfig.node.json new file mode 100644 index 00000000..d3dab4e1 --- /dev/null +++ b/packages/lib/tsconfig.node.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, + }, +} diff --git a/packages/web_ui/package.json b/packages/web_ui/package.json index 6608a14c..0c7c4cc8 100644 --- a/packages/web_ui/package.json +++ b/packages/web_ui/package.json @@ -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", diff --git a/packages/web_ui/src/components/App.tsx b/packages/web_ui/src/components/App.tsx index 02e9c726..e4dcfdc6 100644 --- a/packages/web_ui/src/components/App.tsx +++ b/packages/web_ui/src/components/App.tsx @@ -104,16 +104,16 @@ export default function App(props: AppProps) { } return ( - - + + {page} - - + + ); } diff --git a/packages/web_ui/src/components/ControllerConfigTree.tsx b/packages/web_ui/src/components/ControllerConfigTree.tsx index 71acc44e..a369fab7 100644 --- a/packages/web_ui/src/components/ControllerConfigTree.tsx +++ b/packages/web_ui/src/components/ControllerConfigTree.tsx @@ -27,6 +27,8 @@ export default function ControllerConfigTree() { } catch (err) { return; } + } else { + value = undefined; } await control.send(new lib.ControllerConfigSetPropRequest(field, prop, value)); } diff --git a/packages/web_ui/src/components/HostConfigTree.tsx b/packages/web_ui/src/components/HostConfigTree.tsx index 6f075ac3..aef520f0 100644 --- a/packages/web_ui/src/components/HostConfigTree.tsx +++ b/packages/web_ui/src/components/HostConfigTree.tsx @@ -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)); } diff --git a/packages/web_ui/src/components/HostViewPage.tsx b/packages/web_ui/src/components/HostViewPage.tsx index fc71311d..f70d9e2c 100644 --- a/packages/web_ui/src/components/HostViewPage.tsx +++ b/packages/web_ui/src/components/HostViewPage.tsx @@ -48,7 +48,7 @@ export default function HostViewPage() { let hostButtons = { - account.hasPermission("core.host.revoke_access") + account.hasPermission("core.host.revoke_token") && hostName(instance.assignedHost), + render: (_, instance) => e.stopPropagation()} + > + {hostName(instance.assignedHost)} + , sorter: (a, b) => strcmp(hostName(a.assignedHost), hostName(b.assignedHost)), responsive: ["sm"], }, @@ -68,10 +74,10 @@ export default function InstanceList(props: InstanceListProps) { {publicAddress} - } + ) + && + + } ; } @@ -180,7 +184,7 @@ export default function InstanceViewPage() { return Instance with id {instanceId} was not found on the controller.} type="warning" @@ -198,13 +202,13 @@ export default function InstanceViewPage() { return } + title={instance.name ?? ""} + extra={} /> { - account.hasAllPermission("core.instance.save.list", "core.instance.save.list_subscribe") + account.hasAllPermission("core.instance.save.list", "core.instance.save.subscribe") && } { diff --git a/packages/web_ui/src/components/Link.tsx b/packages/web_ui/src/components/Link.tsx new file mode 100644 index 00000000..224e651d --- /dev/null +++ b/packages/web_ui/src/components/Link.tsx @@ -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) => void; + + return ( + { + onClick?.(event as React.MouseEvent); + if (!event.defaultPrevented) { + handleClick(event); + } + }} + ref={ref as React.RefObject | null} + target={target} + type={type as BaseType} + /> + ); + } +); +export default Link; diff --git a/packages/web_ui/src/components/ModPackViewPage.tsx b/packages/web_ui/src/components/ModPackViewPage.tsx index 47c86fa5..840155f2 100644 --- a/packages/web_ui/src/components/ModPackViewPage.tsx +++ b/packages/web_ui/src/components/ModPackViewPage.tsx @@ -202,7 +202,7 @@ function SearchModsTable(props: SearchModsTableProps) { align: "right", render: (_, result) => + @@ -97,7 +98,9 @@ export default function UsersPage() { title: "Roles", key: "roles", render: (_, user) => ( - [...user.roleIds].map(id => {(roles.get(id) || { name: id }).name}) + [...user.roleIds].map(id => e.stopPropagation()}> + {(roles.get(id) || { name: id }).name} + ) ), }, { diff --git a/packages/web_ui/src/index.ts b/packages/web_ui/src/index.ts index 03e4e6cd..601e092c 100644 --- a/packages/web_ui/src/index.ts +++ b/packages/web_ui/src/index.ts @@ -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"; diff --git a/packages/web_ui/src/model/account.ts b/packages/web_ui/src/model/account.ts index 8086bf3a..dfce6832 100644 --- a/packages/web_ui/src/model/account.ts +++ b/packages/web_ui/src/model/account.ts @@ -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; diff --git a/packages/web_ui/src/pages.tsx b/packages/web_ui/src/pages.tsx index 10d6e0d3..c9aa5b87 100644 --- a/packages/web_ui/src/pages.tsx +++ b/packages/web_ui/src/pages.tsx @@ -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: , }, { diff --git a/packages/web_ui/src/style.css b/packages/web_ui/src/style.css index 7fa95675..3cd12ed2 100644 --- a/packages/web_ui/src/style.css +++ b/packages/web_ui/src/style.css @@ -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; diff --git a/packages/web_ui/tsconfig.web.json b/packages/web_ui/tsconfig.browser.json similarity index 52% rename from packages/web_ui/tsconfig.web.json rename to packages/web_ui/tsconfig.browser.json index 65754630..62dbe09d 100644 --- a/packages/web_ui/tsconfig.web.json +++ b/packages/web_ui/tsconfig.browser.json @@ -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", } } diff --git a/packages/web_ui/tsconfig.json b/packages/web_ui/tsconfig.json new file mode 100644 index 00000000..d96583bf --- /dev/null +++ b/packages/web_ui/tsconfig.json @@ -0,0 +1,6 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.browser.json" }, + ], +} diff --git a/packages/web_ui/webpack.common.js b/packages/web_ui/webpack.common.js index e8a5d8b7..73cae8e8 100644 --- a/packages/web_ui/webpack.common.js +++ b/packages/web_ui/webpack.common.js @@ -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, diff --git a/plugins/global_chat/.npmignore b/plugins/global_chat/.npmignore index ebd99907..55622581 100644 --- a/plugins/global_chat/.npmignore +++ b/plugins/global_chat/.npmignore @@ -1,2 +1,2 @@ webpack.config.js -dist/web/build +dist/browser diff --git a/plugins/global_chat/index.ts b/plugins/global_chat/index.ts index 620a8ecc..97c0985a 100644 --- a/plugins/global_chat/index.ts +++ b/plugins/global_chat/index.ts @@ -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, diff --git a/plugins/global_chat/package.json b/plugins/global_chat/package.json index b438bec3..8f82dc80 100644 --- a/plugins/global_chat/package.json +++ b/plugins/global_chat/package.json @@ -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 ", "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", diff --git a/plugins/global_chat/test/plugin.js b/plugins/global_chat/test/plugin.js index d8b9b860..315df7eb 100644 --- a/plugins/global_chat/test/plugin.js +++ b/plugins/global_chat/test/plugin.js @@ -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() { diff --git a/plugins/global_chat/tsconfig.browser.json b/plugins/global_chat/tsconfig.browser.json new file mode 100644 index 00000000..e1b39d0f --- /dev/null +++ b/plugins/global_chat/tsconfig.browser.json @@ -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", + } +} diff --git a/plugins/global_chat/tsconfig.json b/plugins/global_chat/tsconfig.json index 087576d6..e074e85f 100644 --- a/plugins/global_chat/tsconfig.json +++ b/plugins/global_chat/tsconfig.json @@ -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", - }, } diff --git a/plugins/global_chat/tsconfig.node.json b/plugins/global_chat/tsconfig.node.json new file mode 100644 index 00000000..b6197500 --- /dev/null +++ b/plugins/global_chat/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.node.json", + "references": [ + { "path": "../../packages/lib/tsconfig.node.json" }, + ], + "include": ["./**/*.ts"], + "exclude": ["test/*", "./dist/*"], + "compilerOptions": { + "outDir": "dist/node", + }, +} diff --git a/plugins/global_chat/tsconfig.web.json b/plugins/global_chat/tsconfig.web.json deleted file mode 100644 index c37f9141..00000000 --- a/plugins/global_chat/tsconfig.web.json +++ /dev/null @@ -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", - } -} diff --git a/plugins/inventory_sync/.npmignore b/plugins/inventory_sync/.npmignore index ebd99907..55622581 100644 --- a/plugins/inventory_sync/.npmignore +++ b/plugins/inventory_sync/.npmignore @@ -1,2 +1,2 @@ webpack.config.js -dist/web/build +dist/browser diff --git a/plugins/inventory_sync/controller.ts b/plugins/inventory_sync/controller.ts index 6ce19850..0b4051fe 100644 --- a/plugins/inventory_sync/controller.ts +++ b/plugins/inventory_sync/controller.ts @@ -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 || "-", diff --git a/plugins/inventory_sync/index.ts b/plugins/inventory_sync/index.ts index 029e3a46..a796ee97 100644 --- a/plugins/inventory_sync/index.ts +++ b/plugins/inventory_sync/index.ts @@ -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", diff --git a/plugins/inventory_sync/package.json b/plugins/inventory_sync/package.json index 5664be56..566e4006 100644 --- a/plugins/inventory_sync/package.json +++ b/plugins/inventory_sync/package.json @@ -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 ", "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", diff --git a/plugins/inventory_sync/tsconfig.browser.json b/plugins/inventory_sync/tsconfig.browser.json new file mode 100644 index 00000000..e1b39d0f --- /dev/null +++ b/plugins/inventory_sync/tsconfig.browser.json @@ -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", + } +} diff --git a/plugins/inventory_sync/tsconfig.json b/plugins/inventory_sync/tsconfig.json index 087576d6..e074e85f 100644 --- a/plugins/inventory_sync/tsconfig.json +++ b/plugins/inventory_sync/tsconfig.json @@ -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", - }, } diff --git a/plugins/inventory_sync/tsconfig.node.json b/plugins/inventory_sync/tsconfig.node.json new file mode 100644 index 00000000..b6197500 --- /dev/null +++ b/plugins/inventory_sync/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.node.json", + "references": [ + { "path": "../../packages/lib/tsconfig.node.json" }, + ], + "include": ["./**/*.ts"], + "exclude": ["test/*", "./dist/*"], + "compilerOptions": { + "outDir": "dist/node", + }, +} diff --git a/plugins/inventory_sync/tsconfig.web.json b/plugins/inventory_sync/tsconfig.web.json deleted file mode 100644 index c37f9141..00000000 --- a/plugins/inventory_sync/tsconfig.web.json +++ /dev/null @@ -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", - } -} diff --git a/plugins/player_auth/.npmignore b/plugins/player_auth/.npmignore index ebd99907..55622581 100644 --- a/plugins/player_auth/.npmignore +++ b/plugins/player_auth/.npmignore @@ -1,2 +1,2 @@ webpack.config.js -dist/web/build +dist/browser diff --git a/plugins/player_auth/index.ts b/plugins/player_auth/index.ts index a181c508..62e7b109 100644 --- a/plugins/player_auth/index.ts +++ b/plugins/player_auth/index.ts @@ -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": { diff --git a/plugins/player_auth/package.json b/plugins/player_auth/package.json index a7f78e0a..b65f9186 100644 --- a/plugins/player_auth/package.json +++ b/plugins/player_auth/package.json @@ -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 ", "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", diff --git a/plugins/player_auth/test/plugin.js b/plugins/player_auth/test/plugin.js index 8dc59142..6b90348e 100644 --- a/plugins/player_auth/test/plugin.js +++ b/plugins/player_auth/test/plugin.js @@ -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"); diff --git a/plugins/player_auth/tsconfig.browser.json b/plugins/player_auth/tsconfig.browser.json new file mode 100644 index 00000000..e1b39d0f --- /dev/null +++ b/plugins/player_auth/tsconfig.browser.json @@ -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", + } +} diff --git a/plugins/player_auth/tsconfig.json b/plugins/player_auth/tsconfig.json index 087576d6..e074e85f 100644 --- a/plugins/player_auth/tsconfig.json +++ b/plugins/player_auth/tsconfig.json @@ -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", - }, } diff --git a/plugins/player_auth/tsconfig.node.json b/plugins/player_auth/tsconfig.node.json new file mode 100644 index 00000000..b6197500 --- /dev/null +++ b/plugins/player_auth/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.node.json", + "references": [ + { "path": "../../packages/lib/tsconfig.node.json" }, + ], + "include": ["./**/*.ts"], + "exclude": ["test/*", "./dist/*"], + "compilerOptions": { + "outDir": "dist/node", + }, +} diff --git a/plugins/player_auth/tsconfig.web.json b/plugins/player_auth/tsconfig.web.json deleted file mode 100644 index c37f9141..00000000 --- a/plugins/player_auth/tsconfig.web.json +++ /dev/null @@ -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", - } -} diff --git a/plugins/research_sync/.npmignore b/plugins/research_sync/.npmignore index ebd99907..55622581 100644 --- a/plugins/research_sync/.npmignore +++ b/plugins/research_sync/.npmignore @@ -1,2 +1,2 @@ webpack.config.js -dist/web/build +dist/browser diff --git a/plugins/research_sync/index.ts b/plugins/research_sync/index.ts index af7c8364..42f162d5 100644 --- a/plugins/research_sync/index.ts +++ b/plugins/research_sync/index.ts @@ -6,8 +6,8 @@ export const plugin: lib.PluginDeclaration = { name: "research_sync", title: "Research Sync", description: "Synchronises technology research progress between instances.", - instanceEntrypoint: "dist/plugin/instance", - controllerEntrypoint: "dist/plugin/controller", + instanceEntrypoint: "dist/node/instance", + controllerEntrypoint: "dist/node/controller", messages: [ messages.ContributionEvent, diff --git a/plugins/research_sync/module/sync.lua b/plugins/research_sync/module/sync.lua index 03a1bb9e..7abe268a 100644 --- a/plugins/research_sync/module/sync.lua +++ b/plugins/research_sync/module/sync.lua @@ -137,10 +137,21 @@ function research_sync.sync_technologies(data) for _, tech_data in pairs(game.json_to_table(data)) do local tech = force.technologies[tech_data[nameIndex]] if tech and tech.level <= tech_data[levelIndex] then - tech.level = math.min(tech_data[levelIndex], tech.prototype.max_level) + local new_level = math.min(tech_data[levelIndex], tech.prototype.max_level) + if new_level ~= tech.level then + -- when the level of the current research changes the + -- progress is not automatically reset. + if force.current_research == tech then + force.research_progress = 0 + end + tech.level = new_level + end local progress if tech_data[researchedIndex] then + if force.current_research == tech then + force.research_progress = 0 + end tech.researched = true progress = nil elseif tech_data[progressIndex] then diff --git a/plugins/research_sync/package.json b/plugins/research_sync/package.json index 19ab6b70..ed6c298a 100644 --- a/plugins/research_sync/package.json +++ b/plugins/research_sync/package.json @@ -1,12 +1,12 @@ { "name": "@clusterio/plugin-research_sync", - "version": "2.0.0-alpha.14", + "version": "2.0.0-alpha.17", "description": "Clusterio plugin for synchronizing research between Factorio servers", "author": "Hornwitser ", "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" @@ -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", diff --git a/plugins/research_sync/test/missing.js b/plugins/research_sync/test/missing.js index 790baadb..ae16e8f1 100644 --- a/plugins/research_sync/test/missing.js +++ b/plugins/research_sync/test/missing.js @@ -2,5 +2,5 @@ // required here in order to track coverage for them. "use strict"; -require("../dist/plugin/instance"); -require("../dist/plugin/controller"); +require("../dist/node/instance"); +require("../dist/node/controller"); diff --git a/plugins/research_sync/tsconfig.browser.json b/plugins/research_sync/tsconfig.browser.json new file mode 100644 index 00000000..e1b39d0f --- /dev/null +++ b/plugins/research_sync/tsconfig.browser.json @@ -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", + } +} diff --git a/plugins/research_sync/tsconfig.json b/plugins/research_sync/tsconfig.json index 087576d6..e074e85f 100644 --- a/plugins/research_sync/tsconfig.json +++ b/plugins/research_sync/tsconfig.json @@ -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", - }, } diff --git a/plugins/research_sync/tsconfig.node.json b/plugins/research_sync/tsconfig.node.json new file mode 100644 index 00000000..b6197500 --- /dev/null +++ b/plugins/research_sync/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.node.json", + "references": [ + { "path": "../../packages/lib/tsconfig.node.json" }, + ], + "include": ["./**/*.ts"], + "exclude": ["test/*", "./dist/*"], + "compilerOptions": { + "outDir": "dist/node", + }, +} diff --git a/plugins/research_sync/tsconfig.web.json b/plugins/research_sync/tsconfig.web.json deleted file mode 100644 index c37f9141..00000000 --- a/plugins/research_sync/tsconfig.web.json +++ /dev/null @@ -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", - } -} diff --git a/plugins/statistics_exporter/.npmignore b/plugins/statistics_exporter/.npmignore index ebd99907..55622581 100644 --- a/plugins/statistics_exporter/.npmignore +++ b/plugins/statistics_exporter/.npmignore @@ -1,2 +1,2 @@ webpack.config.js -dist/web/build +dist/browser diff --git a/plugins/statistics_exporter/index.ts b/plugins/statistics_exporter/index.ts index 8748224c..5de5fc80 100644 --- a/plugins/statistics_exporter/index.ts +++ b/plugins/statistics_exporter/index.ts @@ -12,7 +12,7 @@ export const plugin: lib.PluginDeclaration = { description: "Provides in-game item/fluid production, builds, kills, and pollution "+ "statistics to the cluster's Prometheus endpoint.", - instanceEntrypoint: "dist/plugin/instance", + instanceEntrypoint: "dist/node/instance", instanceConfigFields: { "statistics_exporter.command_timeout": { title: "Command Timeout", diff --git a/plugins/statistics_exporter/package.json b/plugins/statistics_exporter/package.json index a98facb8..c1bb9490 100644 --- a/plugins/statistics_exporter/package.json +++ b/plugins/statistics_exporter/package.json @@ -1,12 +1,12 @@ { "name": "@clusterio/plugin-statistics_exporter", - "version": "2.0.0-alpha.14", + "version": "2.0.0-alpha.17", "description": "Clusterio plugin for gathering statistics from Factorio servers", "author": "Hornwitser ", "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", diff --git a/plugins/statistics_exporter/test/plugin.js b/plugins/statistics_exporter/test/plugin.js index e54184a4..18251e27 100644 --- a/plugins/statistics_exporter/test/plugin.js +++ b/plugins/statistics_exporter/test/plugin.js @@ -2,8 +2,8 @@ const assert = require("assert").strict; const mock = require("../../../test/mock"); -const instance = require("../dist/plugin/instance"); -const info = require("../dist/plugin/index").plugin; +const instance = require("../dist/node/instance"); +const info = require("../dist/node/index").plugin; describe("statistics_exporter plugin", function() { diff --git a/plugins/statistics_exporter/tsconfig.browser.json b/plugins/statistics_exporter/tsconfig.browser.json new file mode 100644 index 00000000..f2dc36da --- /dev/null +++ b/plugins/statistics_exporter/tsconfig.browser.json @@ -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", "package.json" ], + "compilerOptions": { + "outDir": "dist/browser", + } +} diff --git a/plugins/statistics_exporter/tsconfig.json b/plugins/statistics_exporter/tsconfig.json index 087576d6..e074e85f 100644 --- a/plugins/statistics_exporter/tsconfig.json +++ b/plugins/statistics_exporter/tsconfig.json @@ -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", - }, } diff --git a/plugins/statistics_exporter/tsconfig.node.json b/plugins/statistics_exporter/tsconfig.node.json new file mode 100644 index 00000000..b6197500 --- /dev/null +++ b/plugins/statistics_exporter/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.node.json", + "references": [ + { "path": "../../packages/lib/tsconfig.node.json" }, + ], + "include": ["./**/*.ts"], + "exclude": ["test/*", "./dist/*"], + "compilerOptions": { + "outDir": "dist/node", + }, +} diff --git a/plugins/statistics_exporter/tsconfig.web.json b/plugins/statistics_exporter/tsconfig.web.json deleted file mode 100644 index decec50c..00000000 --- a/plugins/statistics_exporter/tsconfig.web.json +++ /dev/null @@ -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", "package.json" ], - "compilerOptions": { - "outDir": "dist/web/build", - } -} diff --git a/plugins/subspace_storage/.npmignore b/plugins/subspace_storage/.npmignore index ebd99907..55622581 100644 --- a/plugins/subspace_storage/.npmignore +++ b/plugins/subspace_storage/.npmignore @@ -1,2 +1,2 @@ webpack.config.js -dist/web/build +dist/browser diff --git a/plugins/subspace_storage/index.ts b/plugins/subspace_storage/index.ts index 6543cd6c..1909ef29 100644 --- a/plugins/subspace_storage/index.ts +++ b/plugins/subspace_storage/index.ts @@ -22,7 +22,7 @@ export const plugin: lib.PluginDeclaration = { name: "subspace_storage", title: "Subspace Storage", description: "Provides shared storage across instances for the Subspace Storage mod", - instanceEntrypoint: "dist/plugin/instance", + instanceEntrypoint: "dist/node/instance", instanceConfigFields: { "subspace_storage.log_item_transfers": { title: "Log Item Transfers", @@ -32,7 +32,7 @@ export const plugin: lib.PluginDeclaration = { }, }, - controllerEntrypoint: "dist/plugin/controller", + controllerEntrypoint: "dist/node/controller", controllerConfigFields: { "subspace_storage.division_method": { title: "Division Method", diff --git a/plugins/subspace_storage/instance.ts b/plugins/subspace_storage/instance.ts index 9bc594a8..9771eb26 100644 --- a/plugins/subspace_storage/instance.ts +++ b/plugins/subspace_storage/instance.ts @@ -116,7 +116,6 @@ export class InstancePlugin extends BaseInstancePlugin { let itemsJson = lib.escapeString(JSON.stringify(items)); let task = this.sendRcon(`/sc __subspace_storage__ UpdateInvData("${itemsJson}")`, true); this.pendingTasks.add(task); - task.finally(() => { this.pendingTasks.delete(task); }); - await task; + await task.finally(() => { this.pendingTasks.delete(task); }); } } diff --git a/plugins/subspace_storage/package.json b/plugins/subspace_storage/package.json index 7cd98cf9..22fd77a8 100644 --- a/plugins/subspace_storage/package.json +++ b/plugins/subspace_storage/package.json @@ -1,12 +1,12 @@ { "name": "@clusterio/plugin-subspace_storage", - "version": "2.0.0-alpha.14", + "version": "2.0.0-alpha.17", "description": "Clusterio plugin for sharing storage between Factorio servers", "author": "Hornwitser ", "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" @@ -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/express": "^4.17.17", "@types/fs-extra": "^11.0.1", "@types/node": "^20.4.5", diff --git a/plugins/subspace_storage/test/missing.js b/plugins/subspace_storage/test/missing.js index 6f7e6a1b..ab3009d7 100644 --- a/plugins/subspace_storage/test/missing.js +++ b/plugins/subspace_storage/test/missing.js @@ -2,7 +2,7 @@ // required here in order to track coverage for them. "use strict"; -require("../dist/plugin/instance"); -require("../dist/plugin/controller"); -require("../dist/plugin/dole"); -require("../dist/plugin/dole_nn_base"); +require("../dist/node/instance"); +require("../dist/node/controller"); +require("../dist/node/dole"); +require("../dist/node/dole_nn_base"); diff --git a/plugins/subspace_storage/test/routes.js b/plugins/subspace_storage/test/routes.js index 00de92b0..2753b41e 100644 --- a/plugins/subspace_storage/test/routes.js +++ b/plugins/subspace_storage/test/routes.js @@ -1,7 +1,7 @@ "use strict"; const assert = require("assert").strict; -const routes = require("../dist/plugin/routes"); +const routes = require("../dist/node/routes"); const { get } = require("../../../test/integration"); diff --git a/plugins/subspace_storage/tsconfig.browser.json b/plugins/subspace_storage/tsconfig.browser.json new file mode 100644 index 00000000..e1b39d0f --- /dev/null +++ b/plugins/subspace_storage/tsconfig.browser.json @@ -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", + } +} diff --git a/plugins/subspace_storage/tsconfig.json b/plugins/subspace_storage/tsconfig.json index 087576d6..e074e85f 100644 --- a/plugins/subspace_storage/tsconfig.json +++ b/plugins/subspace_storage/tsconfig.json @@ -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", - }, } diff --git a/plugins/subspace_storage/tsconfig.node.json b/plugins/subspace_storage/tsconfig.node.json new file mode 100644 index 00000000..b6197500 --- /dev/null +++ b/plugins/subspace_storage/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.node.json", + "references": [ + { "path": "../../packages/lib/tsconfig.node.json" }, + ], + "include": ["./**/*.ts"], + "exclude": ["test/*", "./dist/*"], + "compilerOptions": { + "outDir": "dist/node", + }, +} diff --git a/plugins/subspace_storage/tsconfig.web.json b/plugins/subspace_storage/tsconfig.web.json deleted file mode 100644 index c37f9141..00000000 --- a/plugins/subspace_storage/tsconfig.web.json +++ /dev/null @@ -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", - } -} diff --git a/test/controller/HttpCloser.js b/test/controller/HttpCloser.js index 6c48ba2a..d630600b 100644 --- a/test/controller/HttpCloser.js +++ b/test/controller/HttpCloser.js @@ -7,7 +7,7 @@ const util = require("util"); const phin = require("phin"); -const HttpCloser = require("@clusterio/controller/dist/src/HttpCloser").default; +const HttpCloser = require("@clusterio/controller/dist/node/src/HttpCloser").default; // Time to wait during async operations to ensure they happened in order. const tick = 20; diff --git a/test/controller/UserManager.js b/test/controller/UserManager.js index 5294d551..a25067c3 100644 --- a/test/controller/UserManager.js +++ b/test/controller/UserManager.js @@ -1,5 +1,6 @@ "use strict"; const assert = require("assert").strict; +const { PlayerStats } = require("@clusterio/lib"); const { ControllerUser, UserManager } = require("@clusterio/controller"); describe("controller/src/UserManager", function() { @@ -31,5 +32,28 @@ describe("controller/src/UserManager", function() { assert(!userManager.onlineUsers.has(user)); assert.deepEqual(user.instances, new Set()); }); + describe(".clearStatsOfInstance()", function() { + it("should remove the instance stats from existing users", function() { + let user1 = ControllerUser.fromJSON({ name: "admin", roles: [1] }); + let user2 = ControllerUser.fromJSON({ name: "player", roles: [1] }); + userManager.users.set(user1.name, user1); + userManager.users.set(user2.name, user2); + user1.instanceStats.set(10, new PlayerStats({ join_count: 1 })); + user1.instanceStats.set(11, new PlayerStats({ join_count: 6 })); + user1.recalculatePlayerStats(); + userManager.notifyJoin(user1, 11); + user2.instanceStats.set(10, new PlayerStats({ join_count: 3 })); + user2.recalculatePlayerStats(); + assert.equal(user1.playerStats.joinCount, 7); + assert.equal(user2.playerStats.joinCount, 3); + assert(userManager.onlineUsers.has(user1)); + + userManager.clearStatsOfInstance(11); + + assert.equal(user1.playerStats.joinCount, 1); + assert(!userManager.onlineUsers.has(user1)); + assert.equal(user2.playerStats.joinCount, 3); + }); + }); }); }); diff --git a/test/controller/routes.js b/test/controller/routes.js index a598f276..e69449ca 100644 --- a/test/controller/routes.js +++ b/test/controller/routes.js @@ -7,7 +7,7 @@ const http = require("http"); const phin = require("phin"); const { wait } = require("@clusterio/lib"); -const routes = require("@clusterio/controller/dist/src/routes"); +const routes = require("@clusterio/controller/dist/node/src/routes"); const mock = require("../mock"); diff --git a/test/host.js b/test/host.js index 3aa8162e..75244b9e 100644 --- a/test/host.js +++ b/test/host.js @@ -3,8 +3,8 @@ const assert = require("assert").strict; const path = require("path"); const lib = require("@clusterio/lib"); -const Host = require("@clusterio/host/dist/src/Host").default; -const { _discoverInstances } = require("@clusterio/host/dist/src/Host"); +const Host = require("@clusterio/host/dist/node/src/Host").default; +const { _discoverInstances } = require("@clusterio/host/dist/node/src/Host"); describe("Host testing", function() { describe("discoverInstances()", function() { diff --git a/test/host/Instance.js b/test/host/Instance.js index b899a335..9df6a598 100644 --- a/test/host/Instance.js +++ b/test/host/Instance.js @@ -4,7 +4,7 @@ const path = require("path"); const lib = require("@clusterio/lib"); const { PlayerStats, wait } = lib; -const Instance = require("@clusterio/host/dist/src/Instance").default; +const Instance = require("@clusterio/host/dist/node/src/Instance").default; const { MockConnector, MockServer } = require("../mock"); const addr = lib.Address.fromShorthand; diff --git a/test/host/export.js b/test/host/export.js index f99b53ba..b0de6a0a 100644 --- a/test/host/export.js +++ b/test/host/export.js @@ -2,8 +2,8 @@ const assert = require("assert").strict; const path = require("path"); -const { FactorioServer } = require("@clusterio/host/dist/src/server"); -const { _exportLocale } = require("@clusterio/host/dist/src/export"); +const { FactorioServer } = require("@clusterio/host/dist/node/src/server"); +const { _exportLocale } = require("@clusterio/host/dist/node/src/export"); describe("host/src/export", function() { diff --git a/test/host/patch.js b/test/host/patch.js index 9998bccc..8655aabd 100644 --- a/test/host/patch.js +++ b/test/host/patch.js @@ -5,7 +5,7 @@ const JSZip = require("jszip"); const path = require("path"); const lib = require("@clusterio/lib"); -const patch = require("@clusterio/host/dist/src/patch"); +const patch = require("@clusterio/host/dist/node/src/patch"); describe("host/patch", function() { diff --git a/test/host/server.js b/test/host/server.js index 12ade2e0..ea92a06c 100644 --- a/test/host/server.js +++ b/test/host/server.js @@ -4,7 +4,7 @@ const events = require("events"); const fs = require("fs-extra"); const path = require("path"); -const hostServer = require("@clusterio/host/dist/src/server"); +const hostServer = require("@clusterio/host/dist/node/src/server"); const lib = require("@clusterio/lib"); const { wait } = lib; const { testLines } = require("../lib/factorio/lines"); diff --git a/test/integration/clusterio.js b/test/integration/clusterio.js index dcb3c36d..4fe1f626 100644 --- a/test/integration/clusterio.js +++ b/test/integration/clusterio.js @@ -17,6 +17,7 @@ const { } = require("./index"); +/** @returns {Promise>} */ async function getInstances() { let instances = await getControl().send(new lib.InstanceDetailsListRequest()); return new Map(instances.map(instance => [instance.id, instance])); @@ -27,6 +28,10 @@ async function checkInstanceStatus(id, status) { assert.equal(instances.get(44).status, status, "incorrect instance status"); } +async function spawnAltHost(config = "alt-host-config.json") { + return await spawn("alt-host:", `node ../../packages/host run --config ${config}`, /Started host/); +} + async function startAltHost() { let config = "alt-host-config.json"; let configPath = path.join("temp", "test", config); @@ -38,7 +43,7 @@ async function startAltHost() { await exec(`node ../../packages/host --config ${config} config set host.instances_directory alt-instances`); await exec(`node ../../packages/host --config ${config} config set host.factorio_directory ../../factorio`); await exec(`node ../../packages/host --config ${config} config set host.mods_directory alt-mods`); - return await spawn("alt-host:", `node ../../packages/host run --config ${config}`, /Started host/); + return await spawnAltHost(config); } async function stopAltHost(hostProcess) { @@ -200,6 +205,38 @@ describe("Integration of Clusterio", function() { assert(await fs.pathExists(clusterioLib), "mod was not downloaded by the test"); }); }); + it("should auto start instances with auto_start enabled", async function() { + slowTest(this); + + let hostProcess; + try { + hostProcess = await startAltHost(); + await execCtl("instance create alt-start --id 97"); + await execCtl("instance assign alt-start 5"); + await execCtl("instance config set alt-start instance.auto_start true"); + await stopAltHost(hostProcess); + hostProcess = await spawnAltHost(); + // Stop the host immediatly to test the handling of stopping + // instances while automatically started and a save is being created + await stopAltHost(hostProcess); + hostProcess = await spawnAltHost(); + let status; + // Wait for instance status to become running. + for (let i = 0; i < 100; i++) { + const instances = await getInstances(); + status = instances.get(97)?.status ?? "not_present"; + if (status === "running") { + break; + } + await wait(100); // Try again 10 times per second. + } + assert.equal(status, "running"); + await execCtl("instance stop alt-start"); + await execCtl("instance delete alt-start"); + } finally { + await stopAltHost(hostProcess); + } + }); }); describe("clusterioctl", function() { @@ -520,6 +557,15 @@ describe("Integration of Clusterio", function() { slowTest(this); await checkInstanceStatus(44, "running"); }); + it("should allow creating and removing props", async function() { + slowTest(this); + await execCtl("instance config set-prop test factorio.settings new_property new-string-value"); + let config = await getControl().send(new lib.InstanceConfigGetRequest(44)); + assert.equal(config["factorio.settings"]["new_property"], "new-string-value"); + await execCtl("instance config set-prop test factorio.settings new_property"); + config = await getControl().send(new lib.InstanceConfigGetRequest(44)); + assert.equal(config["factorio.settings"]["new_property"], undefined); + }); }); describe("user set-admin/whitelisted/banned", function() { diff --git a/test/integration/export.js b/test/integration/export.js index 3ab962cc..a0d8345a 100644 --- a/test/integration/export.js +++ b/test/integration/export.js @@ -2,8 +2,8 @@ const assert = require("assert").strict; const path = require("path"); -const { _exportLocale } = require("@clusterio/host/dist/src/export"); -const { FactorioServer } = require("@clusterio/host/dist/src/server"); +const { _exportLocale } = require("@clusterio/host/dist/node/src/export"); +const { FactorioServer } = require("@clusterio/host/dist/node/src/server"); describe("Integration of lib/factorio/export", function() { diff --git a/test/integration/index.js b/test/integration/index.js index 6932e283..22450cbd 100644 --- a/test/integration/index.js +++ b/test/integration/index.js @@ -12,11 +12,11 @@ const lib = require("@clusterio/lib"); const { LineSplitter, ConsoleTransport, logger } = lib; // Make sure permissions from plugins are loaded -require("../../plugins/global_chat/dist/plugin/index"); -require("../../plugins/player_auth/dist/plugin/index"); -require("../../plugins/research_sync/dist/plugin/index"); -require("../../plugins/statistics_exporter/dist/plugin/index"); -require("../../plugins/subspace_storage/dist/plugin/index"); +require("../../plugins/global_chat/dist/node/index"); +require("../../plugins/player_auth/dist/node/index"); +require("../../plugins/research_sync/dist/node/index"); +require("../../plugins/statistics_exporter/dist/node/index"); +require("../../plugins/subspace_storage/dist/node/index"); const subspaceStorageVersion = "1.99.8"; const subspaceStorageGitHubTag = "1.99.6"; // Tag subspaceStorageVersion was released under on GitHub diff --git a/test/integration/patch.js b/test/integration/patch.js index bfc1f224..3b2cc72d 100644 --- a/test/integration/patch.js +++ b/test/integration/patch.js @@ -4,7 +4,7 @@ const fs = require("fs-extra"); const JSZip = require("jszip"); const path = require("path"); -const { patch, SaveModule } = require("@clusterio/host/dist/src/patch"); +const { patch, SaveModule } = require("@clusterio/host/dist/node/src/patch"); const lib = require("@clusterio/lib"); const { slowTest } = require("./index"); diff --git a/test/integration/routing.js b/test/integration/routing.js new file mode 100644 index 00000000..6881f079 --- /dev/null +++ b/test/integration/routing.js @@ -0,0 +1,422 @@ +"use strict"; +const assert = require("assert").strict; + +const lib = require("@clusterio/lib"); +const { Host, Instance, InstanceConnection } = require("@clusterio/host"); +const { ControlConnection, Controller, HostConnection, InstanceInfo } = require("@clusterio/controller"); + +const addr = lib.Address.fromShorthand; + +class Control extends lib.Link { } + +/** + * @param {Controller} controller + * @param {number} hostId + */ +function connectHost(controller, hostId, plugins) { + const [controllerSide, hostSide] = lib.VirtualConnector.makePair(addr("controller"), addr({ hostId })); + const registerData = new lib.RegisterHostData("", "0.0.0", hostId, plugins); + const hostConnection = new HostConnection(registerData, controllerSide, controller, "host-a.test"); + controller.wsServer.hostConnections.set(hostId, hostConnection); + const hostConfig = new lib.HostConfig("host"); + hostConfig.set("host.id", hostId); + return new Host(hostSide, "invalid", hostConfig, undefined, []); +} + +/** + * @param {Controller} controller + * @param {Host} host + * @param {number} instanceId + */ +function connectInstance(controller, host, instanceId) { + const hostAddress = addr({ hostId: host.config.get("host.id")}); + const instanceAddress = addr({ instanceId }); + const [instanceSide, hostSide] = lib.VirtualConnector.makePair(instanceAddress, hostAddress); + const instanceConfig = new lib.InstanceConfig("host"); + instanceConfig.set("instance.id", instanceId); + instanceConfig.set("instance.assigned_host", host.config.get("host.id")); + host.instanceInfos.set(instanceId, { path: "invalid", config: instanceConfig }); + controller.instances.set(instanceId, new InstanceInfo(instanceConfig, "stopped")); + const instance = new Instance(host, instanceSide, "invalid", "invalid", instanceConfig); + const instanceConnection = new InstanceConnection(hostSide, host, instance); + host.instanceConnections.set(instanceId, instanceConnection); + return instance; +} + +/** + * @param {Controller} controller + * @param {number} hostId + */ +function connectControl(controller, controlId) { + const [controllerSide, controlSide] = lib.VirtualConnector.makePair(addr("controller"), addr({ controlId })); + const registerData = new lib.RegisterControlData("", "0.0.0"); + let user = controller.userManager.users.get("test"); + if (!user) { + user = controller.userManager.createUser("test"); + } + const controlConnection = new ControlConnection(registerData, controllerSide, controller, user, controlId); + controller.wsServer.controlConnections.set(controlId, controlConnection); + return new Control(controlSide); +} + +class TestRequest { + static type = "request"; + static src = ["controller", "host", "instance", "control"]; + static dst = ["controller", "host", "instance", "control"]; + static permission = null; +} + +class TestEvent { + static type = "event"; + static src = ["controller", "host", "instance", "control"]; + static dst = ["controller", "host", "instance", "control"]; + static permission = null; +} + +class PluginEvent { + static type = "event"; + static src = ["controller", "host", "instance", "control"]; + static dst = ["controller", "host", "instance", "control"]; + static permission = null; + static plugin = "test"; +} + +lib.Link.register(TestRequest); +lib.Link.register(TestEvent); +lib.Link.register(PluginEvent); + +describe("Integration of link routing", function() { + /** @type {Controller} */ + let controller; + /** @type {Host} */ + let hostA; + /** @type {Host} */ + let hostB; + /** @type {Instance} */ + let instanceA1; + /** @type {Instance} */ + let instanceA2; + /** @type {Instance} */ + let instanceB1; + /** @type {Control} */ + let controlA; + /** @type {Control} */ + let controlB; + + beforeEach(function() { + controller = new Controller( + {}, + [], + "invalid", + new lib.ControllerConfig("controller") + ); + hostA = connectHost(controller, 10, []); + hostB = connectHost(controller, 20, []); + instanceA1 = connectInstance(controller, hostA, 11); + instanceA2 = connectInstance(controller, hostA, 12); + instanceB1 = connectInstance(controller, hostB, 21); + controlA = connectControl(controller, 100); + controlB = connectControl(controller, 101); + }); + + /** @returns {Controller | Host | Instance} */ + function get(name) { + return { + controller, + hostA, + hostB, + instanceA1, + instanceA2, + instanceB1, + controlA, + controlB, + }[name]; + } + + // Combinations to try. Equivalent paths have been removed. + const parties = [ + ["controller", ["hostA", "instanceA1", "controlA"]], + ["hostA", ["controller", "hostB", "instanceA1", "instanceB1", "controlA"]], + ["instanceA1", ["controller", "hostA", "hostB", "instanceA2", "instanceB1", "controlA"]], + ["controlA", ["controller", "hostA", "instanceA1", "controlB"]], + ]; + + function handle(dst, Message, handler) { + if (dst instanceof Controller) { + for (const host of dst.wsServer.hostConnections.values()) { + host.handle(Message, handler); + } + for (const control of dst.wsServer.controlConnections.values()) { + control.handle(Message, handler); + } + } else if (dst instanceof Host) { + for (const instance of dst.instanceConnections.values()) { + instance.handle(Message, handler); + } + dst.handle(Message, handler); + } else { + dst.handle(Message, handler); + } + } + + /** + * @param {Controller | Host | Instance} link + * @param {Address} dst + */ + function send(link, dst, message) { + // This logic exists becasue .sendTo doesn't forward all kinds of + // messages to the correct location from all places. Idealy this + // would not be neccessary and .sendTo would do the right thing. + if (link instanceof Host) { + const instance = link.instanceConnections.get(dst.id); + if (instance) { + return instance.sendTo(dst, message); + } + } + return link.sendTo(dst, message); + } + + describe("request routing", function() { + for (const [srcName, dstNames] of parties) { + it(`should route from ${srcName}`, async function() { + for (const dstName of dstNames) { + let called; + const dst = get(dstName); + handle(dst, TestRequest, async () => { called = true; }); + const dstAddr = dst instanceof Controller ? addr("controller") : dst.connector.src; + await send(get(srcName), dstAddr, new TestRequest()); + assert(called, `${dstName} handler was not called`); + } + }); + } + }); + + describe("event routing", function() { + for (const [srcName, dstNames] of parties) { + it(`should route from ${srcName}`, async function() { + for (const dstName of dstNames) { + let called; + const dst = get(dstName); + handle(dst, TestEvent, async () => { called = true; }); + const dstAddr = dst instanceof Controller ? addr("controller") : dst.connector.src; + send(get(srcName), dstAddr, new TestEvent()); + assert(called, `${dstName} handler was not called`); + } + }); + } + }); + + /** + * @param {Controller | Host | Instance | Control} link + * @param {"allHosts" | "allInstances" | "allControls"} dst + */ + function bcast(link, dst, message) { + // This logic exists becasue .sendTo doesn't forward all kinds of + // messages to the correct location from all places. Idealy this + // would not be neccessary and .sendTo would do the right thing. + if (link instanceof Host) { + const plugin = message.constructor.plugin; + if (dst === "allInstances") { + for (const instance of link.instanceConnections.values()) { + if (plugin && !instance.plugins.has(plugin)) { + continue; + } + instance.sendTo(dst, message); + } + } + if (plugin && !link.serverPlugins.has(plugin)) { + return; + } + } + link.sendTo(dst, message); + } + + function bhandle(srcName, dst, Event, canReach = (dstB) => true) { + const asserts = []; + const dsts = { + allHosts: ["hostA", "hostB"], + allInstances: ["instanceA1", "instanceA2", "instanceB1"], + allControls: ["controlA", "controlB"], + }; + for (const dstName of dsts[dst]) { + if (srcName === dstName) { continue; } + // TODO remove when broadcasting from instance to hosts is fixed, see #575 + if (srcName === "instanceA1" && dstName === "hostB") { continue; } + let called; + handle(get(dstName), Event, async () => { called = true; }); + const should = canReach(dstName); + asserts.push(() => assert( + !should ^ called, + `${dstName} handler was ${should ? "not " : ""}called from ${srcName}`, + )); + } + return asserts; + } + + describe("event broadcast", function() { + for (const srcName of ["controller", "hostA", "instanceA1", "controlA"]) { + it(`should broadcast from ${srcName}`, async function() { + for (const dstName of ["allHosts", "allInstances", "allControls"]) { + const dstAsserts = bhandle(srcName, dstName, TestEvent); + bcast(get(srcName), dstName, new TestEvent()); + for (const dstAssert of dstAsserts) { + dstAssert(); + } + } + }); + } + }); + + const graph = { + controller: ["hostA", "hostB", "controlA", "controlB"], + hostA: ["controller", "instanceA1", "instanceA2"], + hostB: ["controller", "instanceB1"], + instanceA1: ["hostA"], + instanceA2: ["hostA"], + instanceB1: ["hostB"], + controlA: ["controller"], + controlB: ["controller"], + }; + + // Returns true if the path from src to dst goes through via + function goesVia(src, dst, via) { + // Places we've been to + const checked = new Set(); + // Places left to check + const paths = new Set([src]); + while (paths.size) { + const [candidate] = paths; + paths.delete(candidate); + if (candidate === via) { + continue; + } + if (checked.has(candidate)) { + continue; + } + if (candidate === dst) { + return false; + } + checked.add(candidate); + for (const newCandidate of graph[candidate]) { + paths.add(newCandidate); + } + } + return true; + } + assert(goesVia("instanceA1", "instanceB1", "controller")); + assert(!goesVia("instanceA1", "instanceA2", "controller")); + assert(goesVia("instanceA1", "hostB", "hostA")); + assert(!goesVia("instanceA1", "hostB", "controlA")); + assert(goesVia("controlA", "controlB", "controller")); + assert(goesVia("controlA", "hostA", "controller")); + + function addPluginExceptFor(exName) { + const ex = get(exName); + const exAddr = ex instanceof Controller ? addr("controller") : ex.connector.src; + for (const name of [ + "controller", "hostA", "hostB", "instanceA1", "instanceA2", "instanceB1", "controlA", "controlB", + ]) { + const party = get(name); + if (party instanceof Controller) { + for (const host of party.wsServer.hostConnections.values()) { + if (!host.connector.dst.equals(exAddr)) { + host.plugins.set("test", "0.0.0"); + } + } + for (const control of party.wsServer.controlConnections.values()) { + if (!control.connector.dst.equals(exAddr)) { + // No way to set plugins + } + } + } else if (party instanceof Host) { + for (const instance of party.instanceConnections.values()) { + if (!instance.connector.dst.equals(exAddr)) { + instance.plugins.set("test", "0.0.0"); + } + } + if (!party.connector.dst.equals(exAddr)) { + party.serverPlugins.set("test", "0.0.0"); + } + } else if (party instanceof Instance) { + // Currently does not handle plugins + } else if (party instanceof Control) { + // Currently does not handle plugins + } + } + } + + // When plugin events are broadcast the party sending the event should + // filter it out if the next hop it is forwarded to does not have plugin + // the event was defined in loaded. + describe("event broadcast with controller missing plugin", function() { + for (const srcName of ["hostA", "instanceA1", "controlA"]) { + it(`should broadcast from ${srcName}`, async function() { + if (srcName === "controlA") { + this.skip(); // Not implemented + } + addPluginExceptFor("controller"); + for (const dstName of ["allHosts", "allInstances", "allControls"]) { + const dstAsserts = bhandle( + srcName, dstName, PluginEvent, dstB => !goesVia(srcName, dstB, "controller"), + ); + bcast(get(srcName), dstName, new PluginEvent()); + for (const dstAssert of dstAsserts) { + dstAssert(); + } + } + }); + } + }); + + describe("event broadcast with host missing plugin", function() { + for (const srcName of ["controller", "hostA", "instanceA1", "controlA"]) { + it(`should broadcast from ${srcName}`, async function() { + addPluginExceptFor("hostB"); + for (const dstName of ["allHosts", "allInstances", "allControls"]) { + const dstAsserts = bhandle( + srcName, dstName, PluginEvent, dstB => !goesVia(srcName, dstB, "hostB"), + ); + bcast(get(srcName), dstName, new PluginEvent()); + for (const dstAssert of dstAsserts) { + dstAssert(); + } + } + }); + } + }); + + describe("event broadcast with instance missing plugin", function() { + for (const srcName of ["controller", "hostA", "instanceA1", "controlA"]) { + it(`should broadcast from ${srcName}`, async function() { + addPluginExceptFor("instanceA2"); + for (const dstName of ["allHosts", "allInstances", "allControls"]) { + const dstAsserts = bhandle( + srcName, dstName, PluginEvent, dstB => !goesVia(srcName, dstB, "instanceA2"), + ); + bcast(get(srcName), dstName, new PluginEvent()); + for (const dstAssert of dstAsserts) { + dstAssert(); + } + } + }); + } + }); + + describe("event broadcast with control missing plugin", function() { + for (const srcName of ["controller", "hostA", "instanceA1", "controlA"]) { + it(`should broadcast from ${srcName}`, async function() { + this.skip(); // Currently not implemented. + addPluginExceptFor("controlB"); + for (const dstName of ["allHosts", "allInstances", "allControls"]) { + const dstAsserts = bhandle( + srcName, dstName, PluginEvent, dstB => !goesVia(srcName, dstB, "controlB"), + ); + bcast(get(srcName), dstName, new PluginEvent()); + for (const dstAssert of dstAsserts) { + dstAssert(); + } + } + }); + } + }); +}); diff --git a/test/integration/server.js b/test/integration/server.js index 0aeac758..9073bbb8 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -5,7 +5,7 @@ const fs = require("fs-extra"); const path = require("path"); const lib = require("@clusterio/lib"); -const { FactorioServer, _getVersion } = require("@clusterio/host/dist/src/server"); +const { FactorioServer, _getVersion } = require("@clusterio/host/dist/node/src/server"); const { logger } = lib; const { slowTest } = require("./index"); diff --git a/test/lib/link/link.js b/test/lib/link/link.js index 0c991031..c8bd06c4 100644 --- a/test/lib/link/link.js +++ b/test/lib/link/link.js @@ -16,6 +16,7 @@ describe("lib/link/link", function() { describe("class Link", function() { let testConnector; + /** @type {lib.Link} */ let testLink; let src = addr({ controlId: 1 }); let dst = addr("controller"); @@ -281,6 +282,7 @@ describe("lib/link/link", function() { let srcReq = new lib.Address(lib.Address.control, 1, 1); testConnector.emit("message", new lib.MessageResponse(1, dst, srcReq)); await request; + assert.deepEqual(testLink._pendingRequests, new Map()); }); it("should send request and reject with error when error response is received", async function() { let message = events.once(testConnector, "send"); @@ -297,6 +299,7 @@ describe("lib/link/link", function() { request, { message: "Error" } ); + assert.deepEqual(testLink._pendingRequests, new Map()); }); it("should send request and resolve with data when response is received", async function() { let message = events.once(testConnector, "send"); @@ -305,6 +308,7 @@ describe("lib/link/link", function() { let srcReq = new lib.Address(lib.Address.control, 1, 1); testConnector.emit("message", new lib.MessageResponse(1, dst, srcReq, 44)); assert.deepEqual(await request, new NumberRequest.Response(44)); + assert.deepEqual(testLink._pendingRequests, new Map()); }); it("should send event to the other side of the link", async function() { let message = events.once(testConnector, "send"); diff --git a/test/lib/lua_tools.js b/test/lib/lua_tools.js index 8fcbac74..8b70b274 100644 --- a/test/lib/lua_tools.js +++ b/test/lib/lua_tools.js @@ -5,6 +5,25 @@ const lib = require("@clusterio/lib"); describe("lib/lua_tools", function() { + describe("normalizeColor()", function() { + it("should normalise colours according to the documentation", function() { + const nc = lib.normalizeColor; + assert.deepEqual(nc({ r: 1, g: 0.2, b: 0.4, a: 0.8 }), { r: 1, g: 0.2, b: 0.4, a: 0.8 }); + assert.deepEqual(nc({ r: 1, g: 0.2, b: 0.4 }), { r: 1, g: 0.2, b: 0.4, a: 1 }); + assert.deepEqual(nc({ r: 1, b: 0.4 }), { r: 1, g: 0, b: 0.4, a: 1 }); + assert.deepEqual(nc({ a: 0.4 }), { r: 0, g: 0, b: 0, a: 0.4 }); + assert.deepEqual(nc({}), { r: 0, g: 0, b: 0, a: 1 }); + assert.deepEqual(nc([1, 0.2, 0.4, 0.8]), { r: 1, g: 0.2, b: 0.4, a: 0.8 }); + assert.deepEqual(nc([1, 0.2, 0.4]), { r: 1, g: 0.2, b: 0.4, a: 1 }); + assert.deepEqual(nc({ r: 255, g: 51, b: 102, a: 204 }), { r: 1, g: 0.2, b: 0.4, a: 0.8 }); + assert.deepEqual(nc({ r: 255, g: 51, b: 102 }), { r: 1, g: 0.2, b: 0.4, a: 1 }); + assert.deepEqual(nc({ r: 255, b: 102 }), { r: 1, g: 0, b: 0.4, a: 1 }); + assert.deepEqual(nc({ a: 102 }), { r: 0, g: 0, b: 0, a: 0.4 }); + assert.deepEqual(nc({}), { r: 0, g: 0, b: 0, a: 1 }); + assert.deepEqual(nc([255, 51, 102, 204]), { r: 1, g: 0.2, b: 0.4, a: 0.8 }); + assert.deepEqual(nc([255, 51, 102]), { r: 1, g: 0.2, b: 0.4, a: 1 }); + }); + }); describe("escapeString()", function() { it("should escape backslashes", function() { assert.equal(lib.escapeString("\\a"), "\\\\a"); diff --git a/test/mock.js b/test/mock.js index a00729c9..1c1fb84b 100644 --- a/test/mock.js +++ b/test/mock.js @@ -5,8 +5,8 @@ const express = require("express"); const lib = require("@clusterio/lib"); -const UserManager = require("@clusterio/controller/dist/src/UserManager").default; -const ControllerUser = require("@clusterio/controller/dist/src/ControllerUser").default; +const UserManager = require("@clusterio/controller/dist/node/src/UserManager").default; +const ControllerUser = require("@clusterio/controller/dist/node/src/ControllerUser").default; const addr = lib.Address.fromShorthand; @@ -73,6 +73,7 @@ class MockConnector extends lib.BaseConnector { super(src, dst); this.connected = true; + this.hasSession = true; this.sentMessages = []; } @@ -80,6 +81,10 @@ class MockConnector extends lib.BaseConnector { this.sentMessages.push(message); setImmediate(() => this.emit("send", message)); } + + sendDisconnectReady() { + this.send(new lib.MessageDisconnect("ready")); + } } class MockServer extends events.EventEmitter { diff --git a/tsconfig.web.json b/tsconfig.browser.json similarity index 89% rename from tsconfig.web.json rename to tsconfig.browser.json index 7edb5691..605c2c61 100644 --- a/tsconfig.web.json +++ b/tsconfig.browser.json @@ -5,7 +5,7 @@ "module": "es2022", "moduleResolution": "bundler", "target": "es2022", - "jsx": "react", + "jsx": "react-jsx", "noEmitOnError": true, }, } diff --git a/tsconfig.json b/tsconfig.json index c9f896d7..df2b7168 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,24 +1,16 @@ { "files": [], "references": [ - { "path": "./packages/lib" }, - { "path": "./packages/lib/tsconfig.web.json" }, - { "path": "./packages/controller" }, - { "path": "./packages/controller/tsconfig.web.json" }, - { "path": "./packages/host" }, - { "path": "./packages/ctl" }, - { "path": "./packages/web_ui/tsconfig.web.json" }, - { "path": "./plugins/global_chat" }, - { "path": "./plugins/global_chat/tsconfig.web.json" }, - { "path": "./plugins/inventory_sync" }, - { "path": "./plugins/inventory_sync/tsconfig.web.json" }, - { "path": "./plugins/player_auth" }, - { "path": "./plugins/player_auth/tsconfig.web.json" }, - { "path": "./plugins/research_sync" }, - { "path": "./plugins/research_sync/tsconfig.web.json" }, - { "path": "./plugins/statistics_exporter" }, - { "path": "./plugins/statistics_exporter/tsconfig.web.json" }, - { "path": "./plugins/subspace_storage" }, - { "path": "./plugins/subspace_storage/tsconfig.web.json" }, + { "path": "./packages/lib/" }, + { "path": "./packages/controller/" }, + { "path": "./packages/host/" }, + { "path": "./packages/ctl/" }, + { "path": "./packages/web_ui/" }, + { "path": "./plugins/global_chat/" }, + { "path": "./plugins/inventory_sync/" }, + { "path": "./plugins/player_auth/" }, + { "path": "./plugins/research_sync/" }, + { "path": "./plugins/statistics_exporter/" }, + { "path": "./plugins/subspace_storage/" }, ], }