mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
Compare commits
74
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f85c0575e | ||
|
|
cf2b3db20d | ||
|
|
a9c6430531 | ||
|
|
8a580a66be | ||
|
|
8ae5e11021 | ||
|
|
fac04adc6f | ||
|
|
d8f1d8a3e2 | ||
|
|
abcbf3fddc | ||
|
|
e5a4d51c86 | ||
|
|
97e615fc21 | ||
|
|
e990134b70 | ||
|
|
c2af8a2377 | ||
|
|
c9609259db | ||
|
|
3cddd2b9f8 | ||
|
|
8879f8dd80 | ||
|
|
4aca5050b9 | ||
|
|
89919f7a89 | ||
|
|
e2da9ac03d | ||
|
|
e8d9eb6916 | ||
|
|
8865509011 | ||
|
|
bcda88710e | ||
|
|
d6b0e88dda | ||
|
|
1021e29c98 | ||
|
|
96082e32b7 | ||
|
|
f70b4f74b1 | ||
|
|
ebb93ada94 | ||
|
|
597d080d71 | ||
|
|
e4f90b3480 | ||
|
|
8c4d2db192 | ||
|
|
152eb7d454 | ||
|
|
8e5d581b9b | ||
|
|
99990c7a0d | ||
|
|
ffd61b961d | ||
|
|
dcf216ca09 | ||
|
|
6bf2bf51f2 | ||
|
|
44b0a8ff83 | ||
|
|
d3f8c993b1 | ||
|
|
535e43b527 | ||
|
|
2df67e32da | ||
|
|
6b85567bc6 | ||
|
|
2e25831b4d | ||
|
|
021af793fc | ||
|
|
7ba14f7433 | ||
|
|
40f5a6ab7c | ||
|
|
48c31b4825 | ||
|
|
520007c8d1 | ||
|
|
c2b6a6e1c0 | ||
|
|
f5255d6f76 | ||
|
|
7d6eac6558 | ||
|
|
6732397061 | ||
|
|
2020024b68 | ||
|
|
499e973db9 | ||
|
|
e3cfc090d3 | ||
|
|
38dcff2225 | ||
|
|
a1109b9354 | ||
|
|
2a57ddb02e | ||
|
|
847945cfdd | ||
|
|
ab88955111 | ||
|
|
fc5701f243 | ||
|
|
256b455315 | ||
|
|
2733f2e3c5 | ||
|
|
96bd5fb1c2 | ||
|
|
123148e277 | ||
|
|
d7edae99c5 | ||
|
|
51fe6c9519 | ||
|
|
e0161114e7 | ||
|
|
2872faab98 | ||
|
|
1d1e80fbf0 | ||
|
|
3d7c11a8b6 | ||
|
|
72264f0c53 | ||
|
|
d6d6c052aa | ||
|
|
901ce4e08e | ||
|
|
5fdc585212 | ||
|
|
0aa330561b |
No files matched your search
+3
-2
@@ -35,6 +35,7 @@ module.exports = {
|
||||
"rules": {
|
||||
"prefer-arrow-callback": "off",
|
||||
"no-invalid-this": "off",
|
||||
"valid-jsdoc": "off",
|
||||
"node/no-unpublished-import": "off",
|
||||
"node/no-unpublished-require": "off",
|
||||
},
|
||||
@@ -319,7 +320,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"],
|
||||
@@ -390,7 +391,7 @@ module.exports = {
|
||||
"node/process-exit-as-throw": "error",
|
||||
"node/shebang": ["error", {
|
||||
"convertPath": {
|
||||
"*.ts": ["^(.+)\\.ts$", "dist/$1.js"],
|
||||
"*.ts": ["^(.+)\\.ts$", "dist/node/$1.js"],
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
@@ -1,5 +1,64 @@
|
||||
# Changelog
|
||||
|
||||
## Version 2.0.0-alpha.17
|
||||
|
||||
### Features
|
||||
|
||||
- Made the host an instance is assigned to link to the host page for that host in the Web UI [#587](https://github.com/clusterio/clusterio/pull/587).
|
||||
- Added pagination to the saves list.
|
||||
|
||||
### Changes
|
||||
|
||||
- Fixed an unhandled error in Subspace Storage crashing the host.
|
||||
- Fixed an unhandled rejection in disconnect logic crashing the host.
|
||||
- Fixed instance screen in Web UI not updating when an instance was reassigned.
|
||||
- Fixed an error thrown in the rendering of one page in the Web UI causing all pages to become inaccessible.
|
||||
- Fixed Web UI not showing instance saves when the permission to list and subscribe the saves is granted but the user is not a cluster admin.
|
||||
- Similarly fixed Web UI not showing the mod pack create, delete, list and host revoke token actions if the user is not a cluster admin.
|
||||
- Fixed Research Sync applying progress of a previous techology level to the level on the controller when it's the active research during instance startup.
|
||||
- Fixed data export with a mod using shorthand notation for the default value of a color-setting causing the ModPack data to be set to invalid data.
|
||||
- Fixed per user per instance player stats not being deleted when the instance it's for is deleted.
|
||||
- Fixed mods setting the item prototype icons property to an object mapping instead of an array breaking the data export.
|
||||
- Fixed a bad mod in the host's mods folder causing instance start to fail instead of the host re-downloading the mod.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Renamed the permission `core.instance.save.list.subscribe` to `core.instance.save.subscribe`.
|
||||
|
||||
Many thanks to the following for contributing to this release:
|
||||
[@Cooldude2606](https://github.com/Cooldude2606)
|
||||
[@Danielv123](https://github.com/Danielv123)
|
||||
[@Hornwitser](https://github.com/Hornwitser)
|
||||
|
||||
|
||||
## Version 2.0.0-alpha.16
|
||||
|
||||
### Changes
|
||||
|
||||
- Fixed message sequences not being renumbered when forwarded between links causing reconnects to send duplicated messages.
|
||||
|
||||
|
||||
## Version 2.0.0-alpha.15
|
||||
|
||||
### Features
|
||||
|
||||
- Added support for `color-settings` Mod settings [#568](https://github.com/clusterio/clusterio/issues/568).
|
||||
- Added `clusterio_controller_pending_requests` and `clusterio_host_pending_requests` metrics.
|
||||
|
||||
### Changes
|
||||
|
||||
- Fixed generated systemd file for clusteriohost not enabling source maps.
|
||||
- Fixed ctl not exporting types used for adding commands to ctl plugins.
|
||||
- Fixed controller only plugins throwing an error about missing a config field on startup out when added to hosts.
|
||||
- Fixed installer incorrectly claiming the options --factorio-dir and --no-download-headless conflicts with each other.
|
||||
- Fixed a memory leak on every request sent over the WebSocket.
|
||||
- Fixed assertion error on closing or invalidating a link with messages forwarded from a virtual link.
|
||||
- Fixed host hanging if stopped while an instance is in the process of creating a save during startup.
|
||||
- Fixed `instance.auto_start` option.
|
||||
- Fixed host log spammed with errors when plugins broadcast messages to instances with the plugin disabled.
|
||||
- Fixed properties of object config fields not being possible to remove.
|
||||
|
||||
|
||||
## Version 2.0.0-alpha.14
|
||||
|
||||
This release does not retain backwards compatibility with 2.0.0-alpha.13 meaning all parts of the cluster needs to updated at the same time.
|
||||
|
||||
@@ -39,6 +39,7 @@ If you are starting a new cluster, it's highly recommended to use the 2.0 alpha.
|
||||
* [Running Clusterio](#running-clusterio)
|
||||
* [Setting up remote hosts](#setting-up-remote-hosts)
|
||||
* [Setting up clusterioctl](#setting-up-clusterioctl)
|
||||
* [Known issues](#known-issues)
|
||||
* [Common problems](#Common-problems)
|
||||
|
||||
|
||||
@@ -265,6 +266,24 @@ If you want to use a different user for the command line interface, you can gene
|
||||
npx clusteriocontroller bootstrap generate-user-token <username>
|
||||
|
||||
|
||||
## Known Issues
|
||||
|
||||
- When the `server_select` plugin is installed you may see `[error] Unhandled event server_select:UpdateInstancesEvent` logged when starting an instance.
|
||||
This should be ignored as it does not indicate an error occured.
|
||||
- After updating Factorio you may get the error `Error: Expected empty response but got "Cannot execute command. Error: [string "clusterio_private.update_instance(..."]:1: attempt to index global 'clusterio_private' (a nil value)?` on instance start.
|
||||
This is due to Factorio applying a migration replacing the patched content of the save with the original scenario, attempting to start the instance again should resolve the issue.
|
||||
- In the mod pack view of the web interface the "base" mod is displayed with an error complaining about the mod being missing from the controller storage.
|
||||
This is due to the "base" mod currently not being connectly handled as a built-in mod to Factorio in the web interface, this error message should be ignored.
|
||||
- After migrating from Alpha 13 to Alpha 14 running an instance data export fails with the error `Upload failed: 401 Unauthorized` due to the host token being issued to a slave.
|
||||
To workaround this find the id of the host in the host config and generate a new host token with the same id.
|
||||
Stop the host and replace the value of the `host.controller_token` field in `config-host.json` with the new host token before starting it up again.
|
||||
- After migrating from Alpha 13 to Alpha 14 you get kicked out from the web interface due to the master to controller rename.
|
||||
If you don't have the authentication token readily available you can manually migrate the token by opening the Inspect/Developer Tools on the page and running the following piece of JavaScript in the Console:
|
||||
```
|
||||
localStorage.controller_token = localStorage.master_token
|
||||
```
|
||||
|
||||
|
||||
## Common problems
|
||||
|
||||
### EACCESS [...] LISTEN 443
|
||||
|
||||
@@ -81,6 +81,9 @@ Note that to use `clusterioctl` you will have to create a control config first:
|
||||
|
||||
Once you've set up the cluster you can use `node packages/controller run` to run the controller and `node packages/host run` to run the host which connects to the controller.
|
||||
|
||||
When editing TypeScript files ran by Node.js you need to run `pnpm build` to compile the files to JavaScript before restarting the controller/host/ctl for changes take effect.
|
||||
To have TypeScript watch for changes and rebuild immediadly you may run `pnpm watch` instead.
|
||||
|
||||
For web development on the controller there are also the following flags:
|
||||
|
||||
`--dev` - Recompile the `web_ui` package on the fly providing live reloading of it.
|
||||
@@ -316,6 +319,6 @@ You will need commit access to the repository as well publish access to the @clu
|
||||
4. Publish the packages.
|
||||
|
||||
```sh
|
||||
# Warning: Double check your current working directory is the repository before running this
|
||||
pnpm -r --filter='!./external_plugins/**' publish
|
||||
rm -r ./{plugins,packages}/*/dist
|
||||
pnpm -r --filter='./{plugins,packages}/*' publish
|
||||
```
|
||||
@@ -78,7 +78,7 @@ import { BaseInstancePlugin } from "@clusterio/host";
|
||||
|
||||
Previously the info of a plugin would be loaded by requiring `./plugin_dir/info`, this has been changed to require `./plugin_dir` which in turn means that `info.js` is no longer the default entrypoint.
|
||||
To solve this you can either add `"main": "info.js",` to package.json or rename `info.js` to `index.js` (the rest of this guide assumes the entrypoint was renamed to `index.js`).
|
||||
The motivation behind this change was to allow TypeScript to output its build to a sub-folder of the plugin, if you're using TypeScript you'll likely have the entrypoint be located in `dist/index.js` instead.
|
||||
The motivation behind this change was to allow TypeScript to output its build to a sub-folder of the plugin, if you're using TypeScript you'll likely have the entrypoint be located in `dist/node/index.js` instead.
|
||||
|
||||
The expected place where the declaration of a plugin is exported has also changed from `module.exports` to `module.exports.plugin`:
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ Mod Pack strings looks something like this:
|
||||
|
||||
If you have multiple mod configuration create a mod pack string for each and note them down.
|
||||
|
||||
Double check that you are on a supported version of Node.js by running `node --version`, the version should be v18 or later, if not you will need to update Node.js before continuing.
|
||||
|
||||
|
||||
## Migrate installations
|
||||
|
||||
@@ -24,7 +26,7 @@ If you have multiple mod configuration create a mod pack string for each and not
|
||||
|
||||
Shutdown all slaves and the master server and then migrate each installations by running the following command:
|
||||
|
||||
npm exec @clusterio/create --migrate-rename --log-level=verbose
|
||||
npm exec @clusterio/create -y -- --migrate-rename --log-level=verbose
|
||||
|
||||
This will convert the existing installation's usage of the master and slave names in packages, configs, database and logs to the new controller and host terms and update the installed packages.
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "tsc --build",
|
||||
"watch": "tsc --build --watch",
|
||||
"test": "pnpm run build && mocha --check-leaks -R spec --recursive test \"plugins/*/test/**\"",
|
||||
"fast-test": "FAST_TEST=y pnpm test",
|
||||
"cover": "nyc pnpm test",
|
||||
@@ -27,6 +28,7 @@
|
||||
"@clusterio/ctl": "workspace:*",
|
||||
"@clusterio/host": "workspace:*",
|
||||
"@clusterio/lib": "workspace:*",
|
||||
"@swc/core": "^1.4.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.4.1",
|
||||
"@typescript-eslint/parser": "^6.4.1",
|
||||
"eslint": "^8.45.0",
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
webpack.config.js
|
||||
dist/web/build
|
||||
dist/browser
|
||||
@@ -37,6 +37,24 @@ import { version } from "./package.json";
|
||||
// globals
|
||||
let controller: Controller;
|
||||
|
||||
void new lib.Gauge(
|
||||
"clusterio_controller_pending_requests",
|
||||
"Count of pending link requests currently waiting in memory on the controller.",
|
||||
{
|
||||
callback: (gauge) => {
|
||||
let count = 0;
|
||||
if (controller) {
|
||||
for (const host of controller.wsServer.hostConnections.values()) {
|
||||
count += host.pendingRequestCount;
|
||||
}
|
||||
for (const control of controller.wsServer.controlConnections.values()) {
|
||||
count += control.pendingRequestCount;
|
||||
}
|
||||
}
|
||||
gauge.set(count);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
void new lib.Gauge(
|
||||
"clusterio_controller_host_mapping",
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { bootstrap } from "./controller";
|
||||
export { default as Controller } from "./src/Controller";
|
||||
export { default as ControllerUser } from "./src/ControllerUser";
|
||||
export { default as ControllerRouter } from "./src/ControllerRouter";
|
||||
export { default as ControlConnection } from "./src/ControlConnection";
|
||||
export { default as HostConnection } from "./src/HostConnection";
|
||||
export { default as InstanceInfo } from "./src/InstanceInfo";
|
||||
export { default as BaseControllerPlugin } from "./src/BaseControllerPlugin";
|
||||
export { default as UserManager } from "./src/UserManager";
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
"description": "Implementation of Clusterio controller",
|
||||
"author": "Hornwitser",
|
||||
"license": "MIT",
|
||||
"version": "2.0.0-alpha.14",
|
||||
"main": "dist/index.js",
|
||||
"version": "2.0.0-alpha.17",
|
||||
"main": "dist/node/index.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build && webpack-cli --env production",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"bin": {
|
||||
"clusteriocontroller": "./dist/controller.js"
|
||||
"clusteriocontroller": "./dist/node/controller.js"
|
||||
},
|
||||
"keywords": [
|
||||
"factorio"
|
||||
@@ -19,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",
|
||||
"busboy": "^1.6.0",
|
||||
"compression": "^1.7.4",
|
||||
@@ -35,7 +35,8 @@
|
||||
"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",
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -26,6 +26,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 +75,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 +274,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"]) {
|
||||
@@ -331,7 +333,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();
|
||||
@@ -735,7 +737,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 +876,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 +941,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 +964,7 @@ export default class Controller {
|
||||
this.instancesDirty = true;
|
||||
|
||||
this.clearSavesOfInstance(instanceId);
|
||||
this.userManager.clearStatsOfInstance(instanceId);
|
||||
|
||||
let prev = instance.status;
|
||||
instance.status = "deleted";
|
||||
@@ -1222,7 +1227,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 +1379,11 @@ export default class Controller {
|
||||
}
|
||||
|
||||
} else if (dst.id === lib.Address.instance || dst.id === lib.Address.host) {
|
||||
const plugin = event.constructor.plugin;
|
||||
for (let hostConnection of this.wsServer.hostConnections.values()) {
|
||||
if (plugin && !hostConnection.plugins.has(plugin)) {
|
||||
continue;
|
||||
}
|
||||
hostConnection.sendEvent(event, dst);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@ import ControlConnection from "./ControlConnection";
|
||||
const { logger } = lib;
|
||||
|
||||
export default class ControllerRouter {
|
||||
pendingRequests: Map<any, any> = new Map();
|
||||
|
||||
constructor(
|
||||
public controller: Controller
|
||||
) {}
|
||||
@@ -16,12 +14,14 @@ export default class ControllerRouter {
|
||||
/**
|
||||
* @param origin - Source link of the message.
|
||||
* @param message - Link the message originated from.
|
||||
* @param entry - message entry if message is a request or an event.
|
||||
* @param hasFallback - true if fallback handling is available.
|
||||
* @returns true if the message was handled
|
||||
*/
|
||||
forwardMessage(
|
||||
origin: ControlConnection | HostConnection,
|
||||
message: lib.MessageRoutable,
|
||||
entry: lib.RequestEntry | lib.EventEntry | undefined,
|
||||
hasFallback: boolean
|
||||
): boolean {
|
||||
if (!["request", "response", "responseError", "event"].includes(message.type)) {
|
||||
@@ -32,7 +32,7 @@ export default class ControllerRouter {
|
||||
let nextHop: ControlConnection | HostConnection | undefined;
|
||||
let msg: string = "";
|
||||
if (dst.type === lib.Address.broadcast) {
|
||||
this.broadcastMessage(origin, message);
|
||||
this.broadcastMessage(origin, message, entry as lib.EventEntry);
|
||||
return true;
|
||||
} else if (dst.type === lib.Address.host) {
|
||||
nextHop = this.controller.wsServer.hostConnections.get(dst.id);
|
||||
@@ -84,7 +84,7 @@ export default class ControllerRouter {
|
||||
if (message.type === "request") {
|
||||
nextHop.forwardRequest(message as lib.MessageRequest, origin);
|
||||
} else {
|
||||
nextHop.connector.send(message);
|
||||
nextHop.connector.forward(message);
|
||||
}
|
||||
} else {
|
||||
this.warnUnrouted(message, msg);
|
||||
@@ -96,20 +96,24 @@ export default class ControllerRouter {
|
||||
broadcastMessage(
|
||||
origin: lib.Link,
|
||||
message: lib.MessageRoutable,
|
||||
entry: lib.EventEntry,
|
||||
) {
|
||||
let dst = message.dst;
|
||||
if (message.type !== "event") {
|
||||
this.warnUnrouted(message, `Unexpected broadcast of ${message.type}`);
|
||||
} else if (dst.id === lib.Address.host || dst.id === lib.Address.instance) {
|
||||
return;
|
||||
}
|
||||
const plugin = entry.Event.plugin;
|
||||
if (dst.id === lib.Address.host || dst.id === lib.Address.instance) {
|
||||
for (let hostConnection of this.controller.wsServer.hostConnections.values()) {
|
||||
if (hostConnection !== origin) {
|
||||
hostConnection.connector.send(message);
|
||||
if (hostConnection !== origin && (!plugin || hostConnection.plugins.has(plugin))) {
|
||||
hostConnection.connector.forward(message);
|
||||
}
|
||||
}
|
||||
} else if (dst.id === lib.Address.control) {
|
||||
for (let controlConnection of this.controller.wsServer.controlConnections.values()) {
|
||||
if (controlConnection !== origin) {
|
||||
controlConnection.connector.send(message);
|
||||
controlConnection.connector.forward(message);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -126,5 +130,3 @@ export default class ControllerRouter {
|
||||
logger.warn(msg ? `${baseMsg}: ${msg}.` : `${baseMsg}.`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ControllerRouter;
|
||||
@@ -178,7 +178,10 @@ export default class HostConnection extends BaseConnection {
|
||||
// changed. Hosts also send status updates on assignInstance which
|
||||
// for hacky reason is also used to push config changes and
|
||||
// restablish status after a connection loss.
|
||||
if (instance.status === request.status) {
|
||||
if (
|
||||
instance.status === request.status
|
||||
&& instance.gamePort === request.gamePort
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -113,4 +113,13 @@ export default class UserManager {
|
||||
this.onlineUsers.delete(user);
|
||||
}
|
||||
}
|
||||
|
||||
clearStatsOfInstance(instanceId: number) {
|
||||
for (const user of this.users.values()) {
|
||||
this.notifyLeave(user, instanceId);
|
||||
user.instanceStats.delete(instanceId);
|
||||
user.recalculatePlayerStats();
|
||||
}
|
||||
this.dirty = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.browser.json",
|
||||
"references": [
|
||||
{ "path": "../lib/tsconfig.browser.json" },
|
||||
{ "path": "../web_ui/tsconfig.browser.json" },
|
||||
],
|
||||
"include": [ "web/**/*.tsx", "web/**/*.ts", "package.json" ],
|
||||
"compilerOptions": {
|
||||
"rootDir": "..",
|
||||
"outDir": "dist/browser",
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.node.json",
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "../lib" },
|
||||
{ "path": "./tsconfig.node.json" },
|
||||
{ "path": "./tsconfig.browser.json" },
|
||||
],
|
||||
"include": ["index.ts", "controller.ts", "package.json", "src/**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.node.json",
|
||||
"references": [
|
||||
{ "path": "../lib/tsconfig.node.json" },
|
||||
],
|
||||
"include": ["index.ts", "controller.ts", "package.json", "src/**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/node",
|
||||
},
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.web.json",
|
||||
"references": [
|
||||
{ "path": "../lib/tsconfig.web.json" },
|
||||
{ "path": "../web_ui/tsconfig.web.json" },
|
||||
],
|
||||
"include": [ "web/**/*.tsx", "web/**/*.ts", "package.json" ],
|
||||
"compilerOptions": {
|
||||
"rootDir": "..",
|
||||
"outDir": "dist/web/build",
|
||||
}
|
||||
}
|
||||
@@ -488,6 +488,7 @@ Group=${await groupIdToName(os.userInfo().gid)}
|
||||
WorkingDirectory=${process.cwd()}
|
||||
KillMode=mixed
|
||||
KillSignal=SIGINT
|
||||
Environment=NODE_OPTIONS=--enable-source-maps
|
||||
ExecStart=${process.cwd()}/node_modules/.bin/clusteriohost run --log-level=warn --can-restart
|
||||
Restart=on-failure
|
||||
RestartPreventExitStatus=8
|
||||
@@ -611,7 +612,7 @@ async function inquirerMissingArgs(args) {
|
||||
},
|
||||
], answers);
|
||||
|
||||
if (answers.downloadHeadless) {
|
||||
if (!answers.factorioDir && answers.downloadHeadless) {
|
||||
answers.factorioDir = "factorio";
|
||||
}
|
||||
}
|
||||
@@ -759,7 +760,7 @@ async function main() {
|
||||
"Can be set to false using --no-download-headless.",
|
||||
type: "boolean",
|
||||
})
|
||||
.conflicts("factorio-dir", "download-headless");
|
||||
;
|
||||
}
|
||||
|
||||
args = args.argv;
|
||||
@@ -779,6 +780,9 @@ async function main() {
|
||||
|
||||
let answers = await inquirerMissingArgs(args);
|
||||
if (answers.downloadHeadless) {
|
||||
if (answers.factorioDir !== "factorio") {
|
||||
throw new InstallError("--download-headless option requires --factorio-dir to be set to factorio");
|
||||
}
|
||||
await downloadLinuxServer();
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { bootstrap } from "./ctl";
|
||||
export * from "./ctl";
|
||||
export { default as BaseCtlPlugin } from "./src/BaseCtlPlugin";
|
||||
|
||||
if (module === require.main) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1097,7 +1097,13 @@ modPackCommands.add(new lib.Command({
|
||||
|
||||
function setModPackSettings(
|
||||
modPack: lib.ModPack,
|
||||
args: { boolSetting?: string[], intSetting?: string[], doubleSetting?: string[], stringSetting?: string[] },
|
||||
args: {
|
||||
boolSetting?: string[],
|
||||
intSetting?: string[],
|
||||
doubleSetting?: string[],
|
||||
stringSetting?: string[],
|
||||
colorSetting?: string[],
|
||||
},
|
||||
) {
|
||||
function doSettings(settings: string[], cast: (value: string) => (string | number | boolean)) {
|
||||
for (let i = 0; i + 2 < settings.length; i += 3) {
|
||||
@@ -1134,6 +1140,13 @@ function setModPackSettings(
|
||||
return number;
|
||||
});
|
||||
doSettings(args.stringSetting || [], value => value);
|
||||
doSettings(args.colorSetting || [], value => {
|
||||
let color = JSON.parse(value);
|
||||
if (typeof color !== "object" || color === null) {
|
||||
throw new lib.CommandError("color value must be a JSON object with r, g, b, a properties");
|
||||
}
|
||||
return color;
|
||||
});
|
||||
}
|
||||
|
||||
function setModPackMods(modPack: lib.ModPack, mods: string[] | undefined) {
|
||||
@@ -1173,6 +1186,7 @@ modPackCommands.add(new lib.Command({
|
||||
"int-setting": { describe: "Set int setting", array: true, nargs: 3, type: "string" },
|
||||
"double-setting": { describe: "Set double setting", array: true, nargs: 3, type: "string" },
|
||||
"string-setting": { describe: "Set string setting", array: true, nargs: 3, type: "string" },
|
||||
"color-setting": { describe: "Set color setting", array: true, nargs: 3, type: "string" },
|
||||
});
|
||||
}],
|
||||
handler: async function(
|
||||
@@ -1186,6 +1200,7 @@ modPackCommands.add(new lib.Command({
|
||||
intSetting?: string[],
|
||||
doubleSetting?: string[],
|
||||
stringSetting?: string[],
|
||||
colorSetting?: string[],
|
||||
},
|
||||
control: Control,
|
||||
) {
|
||||
@@ -1244,6 +1259,7 @@ modPackCommands.add(new lib.Command({
|
||||
"int-setting": { describe: "Set int setting", array: true, nargs: 3, type: "string" },
|
||||
"double-setting": { describe: "Set double setting", array: true, nargs: 3, type: "string" },
|
||||
"string-setting": { describe: "Set string setting", array: true, nargs: 3, type: "string" },
|
||||
"color-setting": { describe: "Set color setting", array: true, nargs: 3, type: "string" },
|
||||
"remove-setting": { describe: "Remove a setting", array: true, nargs: 2, type: "string" },
|
||||
});
|
||||
}],
|
||||
@@ -1261,6 +1277,7 @@ modPackCommands.add(new lib.Command({
|
||||
intSetting?: string[],
|
||||
doubleSetting?: string[],
|
||||
stringSetting?: string[],
|
||||
colorSetting?: string[],
|
||||
removeSetting?: string[],
|
||||
},
|
||||
control: Control,
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
{
|
||||
"extends": "../../tsconfig.node.json",
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "../lib" },
|
||||
{ "path": "./tsconfig.node.json" },
|
||||
],
|
||||
"include": ["index.ts", "package.json", "ctl.ts", "src/**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.node.json",
|
||||
"references": [
|
||||
{ "path": "../lib/tsconfig.node.json" },
|
||||
],
|
||||
"include": ["index.ts", "package.json", "ctl.ts", "src/**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/node",
|
||||
},
|
||||
}
|
||||
+26
-5
@@ -26,6 +26,27 @@ import { ConsoleTransport, levels, logger } from "@clusterio/lib";
|
||||
|
||||
import Host from "./src/Host";
|
||||
|
||||
let host: Host | undefined;
|
||||
|
||||
void new lib.Gauge(
|
||||
"clusterio_host_pending_requests",
|
||||
"Count of pending link requests currently waiting in memory on the host.",
|
||||
{
|
||||
labels: ["host_id"],
|
||||
callback: (gauge) => {
|
||||
if (!host) {
|
||||
return;
|
||||
}
|
||||
|
||||
let count = host.pendingRequestCount;
|
||||
for (const connection of host.instanceConnections.values()) {
|
||||
count += connection.pendingRequestCount;
|
||||
count += connection.instance.pendingRequestCount;
|
||||
}
|
||||
gauge.labels(String(host.config.get("host.id"))).set(count);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export class HostConnector extends lib.WebSocketClientConnector {
|
||||
constructor(
|
||||
@@ -205,7 +226,7 @@ async function startHost() {
|
||||
}
|
||||
|
||||
let hostConnector = new HostConnector(hostConfig, tlsCa, pluginInfos);
|
||||
let host = new Host(
|
||||
host = new Host(
|
||||
hostConnector,
|
||||
args.config,
|
||||
hostConfig,
|
||||
@@ -227,7 +248,7 @@ async function startHost() {
|
||||
|
||||
secondSigint = true;
|
||||
logger.info("Caught interrupt signal, shutting down");
|
||||
host.shutdown();
|
||||
host!.shutdown();
|
||||
});
|
||||
let secondSigterm = false;
|
||||
process.on("SIGTERM", () => {
|
||||
@@ -240,11 +261,11 @@ async function startHost() {
|
||||
|
||||
secondSigterm = true;
|
||||
logger.info("Caught termination signal, shutting down");
|
||||
host.shutdown();
|
||||
host!.shutdown();
|
||||
});
|
||||
process.on("SIGHUP", () => {
|
||||
logger.info("Terminal closed, shutting down");
|
||||
host.shutdown();
|
||||
host!.shutdown();
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -255,7 +276,7 @@ async function startHost() {
|
||||
}
|
||||
|
||||
hostConnector.once("connect", () => {
|
||||
logger.add(new lib.LinkTransport({ link: host }));
|
||||
logger.add(new lib.LinkTransport({ link: host! }));
|
||||
});
|
||||
|
||||
await hostConnector.connect();
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { bootstrap } from "./host";
|
||||
export { default as BaseHostPlugin } from "./src/BaseHostPlugin";
|
||||
export { default as BaseInstancePlugin } from "./src/BaseInstancePlugin";
|
||||
export { default as Host, HostRouter } from "./src/Host";
|
||||
export { default as Instance } from "./src/Instance";
|
||||
export { default as InstanceConnection } from "./src/InstanceConnection";
|
||||
|
||||
if (module === require.main) {
|
||||
bootstrap();
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
"description": "Implementation of Clusterio host server",
|
||||
"author": "Hornwitser",
|
||||
"license": "MIT",
|
||||
"version": "2.0.0-alpha.14",
|
||||
"main": "dist/index.js",
|
||||
"version": "2.0.0-alpha.17",
|
||||
"main": "dist/node/index.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"bin": {
|
||||
"clusteriohost": "./dist/host.js"
|
||||
"clusteriohost": "./dist/node/host.js"
|
||||
},
|
||||
"keywords": [
|
||||
"factorio"
|
||||
@@ -19,7 +19,7 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clusterio/lib": "^2.0.0-alpha.14",
|
||||
"@clusterio/lib": "^2.0.0-alpha.17",
|
||||
"@sinclair/typebox": "^0.30.4",
|
||||
"fs-extra": "^11.1.1",
|
||||
"jimp": "^0.22.8",
|
||||
|
||||
+36
-17
@@ -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") {
|
||||
@@ -377,7 +392,10 @@ export default class Host extends lib.Link {
|
||||
|
||||
async loadPlugins() {
|
||||
for (let pluginInfo of this.pluginInfos) {
|
||||
if (!this.config.get(`${pluginInfo.name}.load_plugin` as keyof lib.HostConfigFields)) {
|
||||
if (
|
||||
!pluginInfo.hostEntrypoint && !pluginInfo.instanceEntrypoint
|
||||
|| !this.config.get(`${pluginInfo.name}.load_plugin` as keyof lib.HostConfigFields)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -582,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)) {
|
||||
@@ -1053,7 +1072,7 @@ export default class Host extends lib.Link {
|
||||
if (instanceInfo.config.get("instance.auto_start")) {
|
||||
try {
|
||||
let instanceConnection = await this._connectInstance(instanceId);
|
||||
await instanceConnection.send(new lib.InstanceStartRequest());
|
||||
await instanceConnection.instance.handleInstanceStartRequest(new lib.InstanceStartRequest());
|
||||
} catch (err: any) {
|
||||
logger.error(
|
||||
`Error during auto startup for ${instanceInfo.config.get("instance.name")}:\n${err.stack}`,
|
||||
|
||||
@@ -800,7 +800,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
|
||||
// Find stand alone modules to load
|
||||
// XXX for now only the included clusterio module is loaded
|
||||
let modulesDirectory = path.join(__dirname, "..", "..", "modules");
|
||||
let modulesDirectory = path.join(__dirname, "..", "..", "..", "modules");
|
||||
for (let entry of await fs.readdir(modulesDirectory, { withFileTypes: true })) {
|
||||
if (entry.isDirectory()) {
|
||||
if (modules.has(entry.name)) {
|
||||
@@ -955,6 +955,11 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
await lib.invokeHook(this.plugins, "onStop");
|
||||
await this.server.stop();
|
||||
await this.sendSaveListUpdate();
|
||||
} else if (
|
||||
this.server._state === "stopping"
|
||||
|| this.server._state === "create"
|
||||
) {
|
||||
await this.server.kill();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+19
-13
@@ -5,7 +5,7 @@ import Jimp from "jimp";
|
||||
import JSZip from "jszip";
|
||||
|
||||
import * as lib from "@clusterio/lib";
|
||||
import * as libBuildMod from "@clusterio/lib/dist/build_mod";
|
||||
import * as libBuildMod from "@clusterio/lib/dist/node/build_mod";
|
||||
import type { FactorioServer } from "./server";
|
||||
|
||||
interface Prototype {
|
||||
@@ -18,11 +18,10 @@ interface SimpleIconSpecification {
|
||||
icon_size: number;
|
||||
icon_mipmaps?: number;
|
||||
}
|
||||
type Color = { r: number, g: number, b: number, a: number };
|
||||
interface IconLayer {
|
||||
icon: string;
|
||||
icon_size: number;
|
||||
tint?: Color,
|
||||
tint?: lib.FactorioColor,
|
||||
shift?: [number, number],
|
||||
scale?: number,
|
||||
icon_mipmaps?: number,
|
||||
@@ -62,7 +61,7 @@ async function generateExportMod(server: FactorioServer) {
|
||||
clean: false,
|
||||
build: true,
|
||||
pack: true,
|
||||
sourceDir: path.join(__dirname, "..", "..", "lua", "export"),
|
||||
sourceDir: path.join(__dirname, "..", "..", "..", "lua", "export"),
|
||||
outputDir: server.writePath("mods"),
|
||||
bumpPatch: false,
|
||||
factorioVersion: server.version.replace(/\.\d+$/, ""),
|
||||
@@ -202,15 +201,9 @@ async function loadLayeredIcon(
|
||||
|
||||
iconLayer = iconLayer.clone();
|
||||
|
||||
let tint: Color;
|
||||
let tint: lib.Color;
|
||||
if (layer.tint) {
|
||||
let divisor = (layer.tint.r > 1 || layer.tint.g > 1 || layer.tint.b > 1) ? 255 : 1;
|
||||
tint = {
|
||||
r: (layer.tint.r || 0) / divisor,
|
||||
g: (layer.tint.g || 0) / divisor,
|
||||
b: (layer.tint.b || 0) / divisor,
|
||||
a: layer.tint.a || 1,
|
||||
};
|
||||
tint = lib.normalizeColor(layer.tint);
|
||||
} else {
|
||||
tint = { r: 1, b: 1, g: 1, a: 1 };
|
||||
}
|
||||
@@ -252,6 +245,18 @@ async function loadLayeredIcon(
|
||||
return icon;
|
||||
}
|
||||
|
||||
function fixIcons(item: ItemPrototype) {
|
||||
const icons = item.icons;
|
||||
if (typeof icons === "object" && !(icons instanceof Array) && icons !== null) {
|
||||
// It's possible to specify icons as an object with arbitrary keys
|
||||
// and the game will still accept it, cast the value to array if
|
||||
// this is the case.
|
||||
item = { ...item };
|
||||
item.icons = Object.values(icons);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
const itemTypes = new Set([
|
||||
"item",
|
||||
"ammo",
|
||||
@@ -284,7 +289,8 @@ const itemTypes = new Set([
|
||||
function filterItems(prototypes: Prototypes): ItemPrototype[] {
|
||||
return Object.entries(prototypes)
|
||||
.filter(([type, _]) => itemTypes.has(type))
|
||||
.flatMap(([_, typePrototypes]) => Object.values(typePrototypes)) as ItemPrototype[]
|
||||
.flatMap(([_, typePrototypes]) => Object.values(typePrototypes) as ItemPrototype[])
|
||||
.map(fixIcons)
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
@@ -865,7 +865,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}`);
|
||||
}
|
||||
@@ -1092,7 +1094,7 @@ export class FactorioServer extends events.EventEmitter {
|
||||
* process.
|
||||
*/
|
||||
async kill(unexpected = false) {
|
||||
this._check(["running", "stopping"]);
|
||||
this._check(["running", "stopping", "create"]);
|
||||
this._killed = true;
|
||||
if (!unexpected) {
|
||||
this._state = "stopping";
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
{
|
||||
"extends": "../../tsconfig.node.json",
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "../lib" },
|
||||
{ "path": "./tsconfig.node.json" },
|
||||
],
|
||||
"include": ["index.ts", "host.ts", "package.json", "src/**/*.ts", "src/**/*.js"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.node.json",
|
||||
"references": [
|
||||
{ "path": "../lib/tsconfig.node.json" },
|
||||
],
|
||||
"include": ["index.ts", "host.ts", "package.json", "src/**/*.ts", "src/**/*.js"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/node",
|
||||
},
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
dist/web/build
|
||||
dist/browser
|
||||
@@ -6,6 +6,7 @@ export * from "./src/factorio/exchange_string";
|
||||
export * from "./src/helpers";
|
||||
export * from "./src/ini";
|
||||
export * from "./src/link";
|
||||
export * from "./src/lua_tools";
|
||||
export * from "./src/logging";
|
||||
export * from "./src/permissions";
|
||||
export * from "./src/plugin";
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "@clusterio/lib",
|
||||
"description": "Shared library for Clusterio",
|
||||
"version": "2.0.0-alpha.14",
|
||||
"version": "2.0.0-alpha.17",
|
||||
"repository": "https://github.com/clusterio/clusterio",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build && pnpm run compile_validator",
|
||||
"compile_validator": "node scripts/compile_validator.js"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"main": "dist/node/index.js",
|
||||
"browser": "browser.ts",
|
||||
"keywords": [
|
||||
"factorio"
|
||||
|
||||
@@ -7,9 +7,9 @@ const standaloneCode = require("ajv/dist/standalone").default;
|
||||
/* eslint-disable node/no-sync */
|
||||
|
||||
// Provide an empty file for message_core to load
|
||||
fs.outputFileSync(path.join(__dirname, "../dist/src/data/message_validate.js"), "");
|
||||
fs.outputFileSync(path.join(__dirname, "../dist/node/src/data/message_validate.js"), "");
|
||||
|
||||
const { Message } = require("../dist/src/data/messages_core");
|
||||
const { Message } = require("../dist/node/src/data/messages_core");
|
||||
|
||||
const ajv = new Ajv({
|
||||
allowUnionTypes: true,
|
||||
@@ -28,5 +28,5 @@ function write(output) {
|
||||
`// generated by scripts/compile_validator.js\n// @ts-nocheck\n/* eslint-disable */\n${moduleCode}`
|
||||
);
|
||||
}
|
||||
write("../dist/src/data/message_validate.js");
|
||||
write("../dist/node/src/data/message_validate.js");
|
||||
write("../src/data/message_validate.js");
|
||||
@@ -459,7 +459,7 @@ export class Config<
|
||||
setProp<Field extends keyof Fields & string>(
|
||||
name: Field,
|
||||
prop: string,
|
||||
value: unknown,
|
||||
value?: unknown,
|
||||
location = this.location,
|
||||
) {
|
||||
let def = this.constructor.fieldDefinitions[name];
|
||||
|
||||
@@ -2,22 +2,31 @@ 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";
|
||||
import type { Logger } from "../logging";
|
||||
|
||||
|
||||
export const ModSettingColor = Type.Object({
|
||||
r: Type.Number(),
|
||||
g: Type.Number(),
|
||||
b: Type.Number(),
|
||||
a: Type.Number(),
|
||||
});
|
||||
export type ModSettingColor = Static<typeof ModSettingColor>;
|
||||
|
||||
/**
|
||||
* A setting for a mod.
|
||||
*/
|
||||
export interface ModSetting {
|
||||
/** Value of the given mod setting. */
|
||||
value: boolean | number | string;
|
||||
value: boolean | number | string | ModSettingColor;
|
||||
}
|
||||
|
||||
const ModSettingJsonSchema = Type.Object({
|
||||
"value": Type.Union([Type.Boolean(), Type.Number(), Type.String()]),
|
||||
"value": Type.Union([Type.Boolean(), Type.Number(), Type.String(), ModSettingColor]),
|
||||
});
|
||||
|
||||
const ModSettingsJsonSchema = Type.Record(Type.String(), ModSettingJsonSchema);
|
||||
@@ -335,7 +344,7 @@ export default class ModPack {
|
||||
* @param logger - Logger used to report warnings on.
|
||||
*/
|
||||
fillDefaultSettings(settingPrototypes: Record<string, object>, logger: Logger) {
|
||||
const knownTypes = ["bool-setting", "int-setting", "double-setting", "string-setting"];
|
||||
const knownTypes = ["bool-setting", "int-setting", "double-setting", "string-setting", "color-setting"];
|
||||
let prototypes = Object.entries(settingPrototypes)
|
||||
.filter(([type, _]) => knownTypes.includes(type))
|
||||
.flatMap(([_, settings]) => Object.values(settings))
|
||||
@@ -355,7 +364,11 @@ export default class ModPack {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.settings[settingType].set(prototype.name, { value: prototype.default_value });
|
||||
let value = prototype.default_value;
|
||||
if (prototype.type === "color-setting") {
|
||||
value = libLuaTools.normalizeColor(value);
|
||||
}
|
||||
this.settings[settingType].set(prototype.name, { value });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,12 +19,20 @@ export default class Role {
|
||||
permissions: Type.Array(Type.String()),
|
||||
});
|
||||
|
||||
static remapPermission(permission: string) {
|
||||
// migrate: core.instance.save.list.subscribe was renamed in alpha 17
|
||||
if (permission === "core.instance.save.list.subscribe") {
|
||||
return "core.instance.save.subscribe";
|
||||
}
|
||||
return permission;
|
||||
}
|
||||
|
||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||
return new this(
|
||||
json.id,
|
||||
json.name,
|
||||
json.description,
|
||||
new Set(json.permissions),
|
||||
new Set(json.permissions.map(this.remapPermission)),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
export { default as ExportManifest } from "./ExportManifest";
|
||||
export { default as ModInfo } from "./ModInfo";
|
||||
export { default as ModPack, ModSetting, ModRecord } from "./ModPack";
|
||||
export { default as ModPack, ModSetting, ModSettingColor, ModRecord } from "./ModPack";
|
||||
export { default as ModuleInfo } from "./ModuleInfo";
|
||||
export { default as Permission } from "./Permission";
|
||||
export { default as PlayerStats } from "./PlayerStats";
|
||||
|
||||
@@ -87,7 +87,7 @@ export class Address {
|
||||
}
|
||||
|
||||
toString() {
|
||||
const typeMap = ["controller", "instance", "host", "control"];
|
||||
const typeMap = ["controller", "host", "instance", "control"];
|
||||
const type = typeMap[this.type] || this.type;
|
||||
if (this.type === Address.broadcast) {
|
||||
return `[Address ${type}:${typeMap[this.id] || this.id}]`;
|
||||
|
||||
@@ -208,7 +208,7 @@ export class InstanceConfigSetPropRequest {
|
||||
"instanceId": Type.Integer(),
|
||||
"field": Type.String(),
|
||||
"prop": Type.String(),
|
||||
"value": Type.Unknown(),
|
||||
"value": Type.Optional(Type.Unknown()),
|
||||
});
|
||||
|
||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||
@@ -361,7 +361,7 @@ export class InstanceSaveDetailsUpdatesEvent {
|
||||
static type = "event" as const;
|
||||
static src = ["instance", "host", "controller"] as const;
|
||||
static dst = ["controller", "control"] as const;
|
||||
static permission = "core.instance.save.list.subscribe" as const;
|
||||
static permission = "core.instance.save.subscribe" as const;
|
||||
|
||||
constructor(
|
||||
public updates: SaveDetails[],
|
||||
@@ -385,7 +385,7 @@ export class InstanceSaveDetailsUpdatesEvent {
|
||||
export class InstanceCreateSaveRequest {
|
||||
declare ["constructor"]: typeof InstanceCreateSaveRequest;
|
||||
static type = "request" as const;
|
||||
static src = "control" as const;
|
||||
static src = ["control", "controller"] as const;
|
||||
static dst = "instance" as const;
|
||||
static permission = "core.instance.save.create" as const;
|
||||
|
||||
@@ -596,7 +596,7 @@ export class InstancePushSaveRequest {
|
||||
export class InstanceLoadScenarioRequest {
|
||||
declare ["constructor"]: typeof InstanceLoadScenarioRequest;
|
||||
static type = "request" as const;
|
||||
static src = "control" as const;
|
||||
static src = ["control", "controller"] as const;
|
||||
static dst = "instance" as const;
|
||||
static permission = "core.instance.load_scenario" as const;
|
||||
|
||||
@@ -622,7 +622,7 @@ export class InstanceLoadScenarioRequest {
|
||||
export class InstanceExportDataRequest {
|
||||
declare ["constructor"]: typeof InstanceExportDataRequest;
|
||||
static type = "request" as const;
|
||||
static src = "control" as const;
|
||||
static src = ["control", "controller"] as const;
|
||||
static dst = "instance" as const;
|
||||
static permission = "core.instance.export_data" as const;
|
||||
}
|
||||
@@ -630,7 +630,7 @@ export class InstanceExportDataRequest {
|
||||
export class InstanceExtractPlayersRequest {
|
||||
declare ["constructor"]: typeof InstanceExtractPlayersRequest;
|
||||
static type = "request" as const;
|
||||
static src = "control" as const;
|
||||
static src = ["control", "controller"] as const;
|
||||
static dst = "instance" as const;
|
||||
static permission = "core.instance.extract_players" as const;
|
||||
}
|
||||
@@ -638,7 +638,7 @@ export class InstanceExtractPlayersRequest {
|
||||
export class InstanceStopRequest {
|
||||
declare ["constructor"]: typeof InstanceStopRequest;
|
||||
static type = "request" as const;
|
||||
static src = "control" as const;
|
||||
static src = ["control", "controller"] as const;
|
||||
static dst = "instance" as const;
|
||||
static permission = "core.instance.stop" as const;
|
||||
}
|
||||
@@ -646,7 +646,7 @@ export class InstanceStopRequest {
|
||||
export class InstanceKillRequest {
|
||||
declare ["constructor"]: typeof InstanceKillRequest;
|
||||
static type = "request" as const;
|
||||
static src = "control" as const;
|
||||
static src = ["control", "controller"] as const;
|
||||
static dst = "instance" as const;
|
||||
static permission = "core.instance.kill" as const;
|
||||
}
|
||||
@@ -693,7 +693,7 @@ export class InstanceDeleteInternalRequest {
|
||||
export class InstanceSendRconRequest {
|
||||
declare ["constructor"]: typeof InstanceSendRconRequest;
|
||||
static type = "request" as const;
|
||||
static src = "control" as const;
|
||||
static src = ["control", "controller"] as const;
|
||||
static dst = "instance" as const;
|
||||
static permission = "core.instance.send_rcon" as const;
|
||||
|
||||
|
||||
@@ -33,7 +33,30 @@ export abstract class BaseConnector extends events.EventEmitter {
|
||||
this._seq = 1;
|
||||
}
|
||||
|
||||
abstract send(message: libData.Message): void;
|
||||
protected abstract send(message: libData.MessageRoutable): void;
|
||||
|
||||
forward(message: libData.MessageRoutable) {
|
||||
const seq = this._seq;
|
||||
this._seq += 1;
|
||||
// The message type currently can't be inferred like a tagged union
|
||||
// TODO fix typing of message so it can be deduced from .type
|
||||
if (message.type === "request") {
|
||||
const request = message as libData.MessageRequest;
|
||||
this.send(new libData.MessageRequest(seq, request.src, request.dst, request.name, request.data));
|
||||
} else if (message.type === "response") {
|
||||
const response = message as libData.MessageResponse;
|
||||
this.send(new libData.MessageResponse(seq, response.src, response.dst, response.data));
|
||||
} else if (message.type === "responseError") {
|
||||
const error = message as libData.MessageResponseError;
|
||||
this.send(new libData.MessageResponseError(seq, error.src, error.dst, error.data));
|
||||
} else if (message.type === "event") {
|
||||
const event = message as libData.MessageEvent;
|
||||
this.send(new libData.MessageEvent(seq, event.src, event.dst, event.name, event.data));
|
||||
} else {
|
||||
throw new Error(`Cannot forward message type ${(message as any).type}`);
|
||||
}
|
||||
return seq;
|
||||
}
|
||||
|
||||
sendRequest<Req, Res>(
|
||||
request: Request<Req, Res>,
|
||||
@@ -242,7 +265,7 @@ export abstract class WebSocketBaseConnector extends BaseConnector {
|
||||
*
|
||||
* @param message - Message to send.
|
||||
*/
|
||||
send(message: libData.MessageRoutable) {
|
||||
protected send(message: libData.MessageRoutable) {
|
||||
if (!["connected", "resuming"].includes(this._state)) {
|
||||
throw new libErrors.SessionLost("No session");
|
||||
}
|
||||
@@ -295,6 +318,13 @@ export abstract class WebSocketBaseConnector extends BaseConnector {
|
||||
await this.close(1000, "Disconnect");
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify other end that the link is ready to disconnect
|
||||
*/
|
||||
sendDisconnectReady() {
|
||||
this._sendInternal(new libData.MessageDisconnect("ready"));
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the connection is established and active
|
||||
*/
|
||||
|
||||
@@ -72,7 +72,12 @@ export type RequestHandler<Req, Res> = (request: Req, src: libData.Address, dst:
|
||||
export type EventHandler<T> = (event: T, src: libData.Address, dst: libData.Address) => Promise<void>;
|
||||
|
||||
interface Router {
|
||||
forwardMessage(origin: Link, message: MessageRoutable, fallback: boolean): boolean,
|
||||
forwardMessage(
|
||||
origin: Link,
|
||||
message: MessageRoutable,
|
||||
entry: RequestEntry | EventEntry | undefined,
|
||||
fallback: boolean,
|
||||
): boolean,
|
||||
}
|
||||
|
||||
// Some definitions for the terminology used here:
|
||||
@@ -121,15 +126,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 +146,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 +348,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 +439,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 +456,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 +561,7 @@ export class Link {
|
||||
dst: message.dst,
|
||||
};
|
||||
this._forwardedRequests.set(message.src.index(), pending);
|
||||
this.connector.send(message);
|
||||
this.connector.forward(message);
|
||||
}
|
||||
|
||||
sendEvent<T>(event: Event<T>, dst: libData.Address) {
|
||||
|
||||
@@ -73,7 +73,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 +163,7 @@ export class TerminalFormat {
|
||||
src += `i:${info.instance_name} `;
|
||||
|
||||
} else if (info.host_name) {
|
||||
src += `s:${info.host_name} `;
|
||||
src += `h:${info.host_name} `;
|
||||
}
|
||||
if (info.plugin) {
|
||||
src += `${info.plugin}: `;
|
||||
|
||||
@@ -3,6 +3,54 @@
|
||||
* @module lib/lua_tools
|
||||
*/
|
||||
|
||||
/**
|
||||
* A colour as it may originate from Factorio Lua code, see
|
||||
* https://lua-api.factorio.com/latest/types/Color.html
|
||||
*/
|
||||
export type FactorioColor = { r?: number, g?: number, b?: number, a?: number } | [number, number, number, number?];
|
||||
|
||||
export type Color = { r: number, g: number, b: number, a: number };
|
||||
|
||||
/**
|
||||
* Normalise a color as it may appear in Factorio Lua code to be an object
|
||||
* with r, g, b, a properties in the range 0 to 1.
|
||||
*
|
||||
* @param color -
|
||||
* A colour that may be specified with left out components or as an
|
||||
* array and have the range 1 to 0 or 255 to 0.
|
||||
* @returns
|
||||
* Normalised colour where all 4 components are present as properties in
|
||||
* the range 0 to 1.
|
||||
*/
|
||||
export function normalizeColor(color: FactorioColor) {
|
||||
let r, g, b, a;
|
||||
if (color instanceof Array) {
|
||||
([r, g, b, a] = color);
|
||||
} else {
|
||||
({ r, g, b, a } = color);
|
||||
}
|
||||
|
||||
r ??= NaN;
|
||||
g ??= NaN;
|
||||
b ??= NaN;
|
||||
a ??= NaN;
|
||||
|
||||
// Note NaN > 1 is false and NaN / 255 is NaN
|
||||
if (r > 1 || g > 1 || b > 1 || a > 1) {
|
||||
r /= 255;
|
||||
g /= 255;
|
||||
b /= 255;
|
||||
a /= 255;
|
||||
}
|
||||
|
||||
return {
|
||||
r: Number.isNaN(r) ? 0 : r,
|
||||
g: Number.isNaN(g) ? 0 : g,
|
||||
b: Number.isNaN(b) ? 0 : b,
|
||||
a: Number.isNaN(a) ? 1 : a,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes a string for inclusion into a lua string
|
||||
*
|
||||
|
||||
@@ -239,11 +239,11 @@ definePermission({
|
||||
definePermission({
|
||||
name: "core.instance.save.list",
|
||||
title: "List saves",
|
||||
description: "List the saves currently on the instance.",
|
||||
description: "List the saves currently on instances.",
|
||||
});
|
||||
definePermission({
|
||||
name: "core.instance.save.list.subscribe",
|
||||
title: "Subscribe to save list updatess",
|
||||
name: "core.instance.save.subscribe",
|
||||
title: "Subscribe to save updates",
|
||||
description: "Subscribe to be notifed on updates to the list of saves of instances.",
|
||||
});
|
||||
definePermission({
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Type, Static } from "@sinclair/typebox";
|
||||
import { Link, Event, EventClass, RequestHandler, WebSocketClientConnector, WebSocketBaseConnector } from "./link";
|
||||
import { logger } from "./logging";
|
||||
import { Address, MessageRequest, IControllerUser } from "./data";
|
||||
import isDeepStrictEqual from "./is_deep_strict_equal";
|
||||
|
||||
export type SubscriptionRequestHandler<T> = RequestHandler<SubscriptionRequest, Event<T> | null>;
|
||||
export type EventSubscriberCallback<T> = (updates: T[], synced: boolean) => void
|
||||
@@ -204,6 +205,14 @@ export class EventSubscriber<
|
||||
async _handle(event: EventSubscribable<T, V>) {
|
||||
for (const value of event.updates) {
|
||||
this.lastResponseTimeMs = Math.max(this.lastResponseTimeMs, value.updatedAtMs);
|
||||
// Warn about updates which changes content but don't update updatedAtMs
|
||||
const existing = this.values.get(value.id as K);
|
||||
if (existing && existing.updatedAtMs === value.updatedAtMs && !isDeepStrictEqual(existing, value)) {
|
||||
logger.warn(
|
||||
`${this.Event.name} contains update for value with id ${value.id} that has an identical ` +
|
||||
"updatedAtMs timestamp but differing content"
|
||||
);
|
||||
}
|
||||
if (value.isDeleted) {
|
||||
this.values.delete(value.id as K);
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig.web.json",
|
||||
"extends": "../../tsconfig.browser.json",
|
||||
"include": [
|
||||
"browser.ts",
|
||||
"package.json",
|
||||
@@ -12,6 +12,7 @@
|
||||
"./src/hash.ts",
|
||||
"./src/ini.ts",
|
||||
"./src/link/*.ts",
|
||||
"./src/lua_tools.ts",
|
||||
"./src/logging.ts",
|
||||
"./src/permissions.ts",
|
||||
"./src/prometheus.ts",
|
||||
@@ -25,6 +26,6 @@
|
||||
],
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"outDir": "dist/web/build",
|
||||
"outDir": "dist/browser",
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.node.json",
|
||||
"include": ["index.ts", "browser.ts", "build_mod.js", "src/**/*.ts", "src/**/*.js"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"allowJs": true,
|
||||
},
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.node.json" },
|
||||
{ "path": "./tsconfig.browser.json" },
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.node.json",
|
||||
"include": ["index.ts", "browser.ts", "build_mod.js", "src/**/*.ts", "src/**/*.js"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/node",
|
||||
"allowJs": true,
|
||||
},
|
||||
}
|
||||
@@ -3,9 +3,9 @@
|
||||
"description": "Clusterio web interface implementation",
|
||||
"author": "Hornwitser",
|
||||
"license": "MIT",
|
||||
"version": "2.0.0-alpha.14",
|
||||
"version": "2.0.0-alpha.17",
|
||||
"browser": "src/index.ts",
|
||||
"main": "dist/src/index.js",
|
||||
"main": "dist/browser/src/index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
@@ -17,12 +17,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.1.4",
|
||||
"@babel/core": "^7.22.9",
|
||||
"@babel/preset-react": "^7.22.5",
|
||||
"@clusterio/lib": "^2.0.0-alpha.14",
|
||||
"@clusterio/lib": "^2.0.0-alpha.17",
|
||||
"@swc/core": "^1.4.0",
|
||||
"antd": "^5.13.0",
|
||||
"assert": "^2.0.0",
|
||||
"babel-loader": "^9.1.3",
|
||||
"browserify-zlib": "^0.2.0",
|
||||
"buffer": "^6.0.3",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
@@ -39,8 +37,8 @@
|
||||
"react-router-dom": "^6.14.2",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"style-loader": "^3.3.3",
|
||||
"swc-loader": "^0.2.6",
|
||||
"terser-webpack-plugin": "^5.3.9",
|
||||
"ts-loader": "^9.4.4",
|
||||
"typescript": "^5.1.6",
|
||||
"util": "^0.12.5",
|
||||
"webpack": "^5.88.2",
|
||||
|
||||
@@ -104,16 +104,16 @@ export default function App(props: AppProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary Component={ErrorCard}>
|
||||
<ConfigProvider
|
||||
theme={{ algorithm: theme.darkAlgorithm }}
|
||||
>
|
||||
<ConfigProvider
|
||||
theme={{ algorithm: theme.darkAlgorithm }}
|
||||
>
|
||||
<ErrorBoundary Component={ErrorCard}>
|
||||
<ControlContext.Provider value={props.control}>
|
||||
<BrowserRouter basename={webRoot}>
|
||||
{page}
|
||||
</BrowserRouter>
|
||||
</ControlContext.Provider>
|
||||
</ConfigProvider>
|
||||
</ErrorBoundary>
|
||||
</ErrorBoundary>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
@@ -27,6 +27,8 @@ export default function ControllerConfigTree() {
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
value = undefined;
|
||||
}
|
||||
await control.send(new lib.ControllerConfigSetPropRequest(field, prop, value));
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ export default function HostConfigTree(props: { id: number, available: boolean }
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
value = undefined;
|
||||
}
|
||||
await control.sendTo({ hostId: props.id }, new lib.HostConfigSetPropRequest(field, prop, value));
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function HostViewPage() {
|
||||
|
||||
let hostButtons = <Space>
|
||||
{
|
||||
account.hasPermission("core.host.revoke_access")
|
||||
account.hasPermission("core.host.revoke_token")
|
||||
&& <Popconfirm
|
||||
title={`Revoke tokens of ${host.name}?`}
|
||||
placement="bottomRight"
|
||||
|
||||
@@ -28,6 +28,8 @@ export default function InstanceConfigTree(props: InstanceConfigTreeProps) {
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
value = undefined;
|
||||
}
|
||||
await control.send(new lib.InstanceConfigSetPropRequest(props.id, field, prop, value));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { message, Button, Space, Table } from "antd";
|
||||
import { message, Button, Space, Table, Typography } from "antd";
|
||||
import CopyOutlined from "@ant-design/icons/CopyOutlined";
|
||||
import type { SizeType } from "antd/es/config-provider/SizeContext";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
@@ -10,6 +10,7 @@ import { useHosts } from "../model/host";
|
||||
import InstanceStatusTag from "./InstanceStatusTag";
|
||||
import StartStopInstanceButton from "./StartStopInstanceButton";
|
||||
import { InstanceDetails } from "@clusterio/lib";
|
||||
import Link from "./Link";
|
||||
|
||||
const strcmp = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }).compare;
|
||||
|
||||
@@ -55,7 +56,12 @@ export default function InstanceList(props: InstanceListProps) {
|
||||
{
|
||||
title: "Assigned Host",
|
||||
key: "assignedHost",
|
||||
render: (_, instance) => hostName(instance.assignedHost),
|
||||
render: (_, instance) => <Link
|
||||
to={`/hosts/${instance.assignedHost}/view`}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
{hostName(instance.assignedHost)}
|
||||
</Link>,
|
||||
sorter: (a, b) => strcmp(hostName(a.assignedHost), hostName(b.assignedHost)),
|
||||
responsive: ["sm"],
|
||||
},
|
||||
@@ -68,10 +74,10 @@ export default function InstanceList(props: InstanceListProps) {
|
||||
{publicAddress}
|
||||
<Button
|
||||
type="text"
|
||||
icon={<CopyOutlined/>}
|
||||
icon={<CopyOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigator.clipboard.writeText(publicAddress??"");
|
||||
navigator.clipboard.writeText(publicAddress ?? "");
|
||||
message.success("Copied public address!");
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -23,6 +23,7 @@ import { notifyErrorHandler } from "../util/notify";
|
||||
import { useInstance } from "../model/instance";
|
||||
import { useHost } from "../model/host";
|
||||
import InstanceStatusTag from "./InstanceStatusTag";
|
||||
import Link from "./Link";
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
@@ -42,7 +43,9 @@ function InstanceDescription(props: InstanceDescriptionProps) {
|
||||
<Descriptions.Item label="Host">
|
||||
{!assigned
|
||||
? <em>Unassigned</em>
|
||||
: host?.name ?? instance.assignedHost
|
||||
: <Link to={`/hosts/${host?.id ?? instance.assignedHost}/view`}>
|
||||
{host?.name ?? instance.assignedHost}
|
||||
</Link>
|
||||
}
|
||||
{account.hasPermission("core.instance.assign") && <AssignInstanceModal
|
||||
id={instance.id}
|
||||
@@ -104,7 +107,7 @@ function InstanceButtons(props: { instance: lib.InstanceDetails }) {
|
||||
}
|
||||
let instanceButtonsMenuProps: MenuProps = {
|
||||
items: instanceButtonMenuItems,
|
||||
onClick: ({ key }: { key:string }) => {
|
||||
onClick: ({ key }: { key: string }) => {
|
||||
if (key === "export") {
|
||||
setExportingData(true);
|
||||
control.sendTo(
|
||||
@@ -156,9 +159,10 @@ function InstanceButtons(props: { instance: lib.InstanceDetails }) {
|
||||
"core.instance.extract_players",
|
||||
"core.instance.kill",
|
||||
"core.instance.delete",
|
||||
) && <Dropdown placement="bottomRight" trigger={["click"]} menu={instanceButtonsMenuProps}>
|
||||
<Button>More <DownOutlined /></Button>
|
||||
</Dropdown>}
|
||||
)
|
||||
&& <Dropdown placement="bottomRight" trigger={["click"]} menu={instanceButtonsMenuProps}>
|
||||
<Button>More <DownOutlined /></Button>
|
||||
</Dropdown>}
|
||||
</Space>;
|
||||
}
|
||||
|
||||
@@ -180,7 +184,7 @@ export default function InstanceViewPage() {
|
||||
|
||||
return <PageLayout nav={nav}>
|
||||
<Alert
|
||||
message={"Instance not found" }
|
||||
message={"Instance not found"}
|
||||
showIcon
|
||||
description={<>Instance with id {instanceId} was not found on the controller.</>}
|
||||
type="warning"
|
||||
@@ -198,13 +202,13 @@ export default function InstanceViewPage() {
|
||||
|
||||
return <PageLayout nav={nav}>
|
||||
<PageHeader
|
||||
title={instance.name??""}
|
||||
extra={<InstanceButtons instance={instance}/>}
|
||||
title={instance.name ?? ""}
|
||||
extra={<InstanceButtons instance={instance} />}
|
||||
/>
|
||||
<InstanceDescription host={host} instance={instance} />
|
||||
|
||||
{
|
||||
account.hasAllPermission("core.instance.save.list", "core.instance.save.list_subscribe")
|
||||
account.hasAllPermission("core.instance.save.list", "core.instance.save.subscribe")
|
||||
&& <SavesList instance={instance} />
|
||||
}
|
||||
{
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from "react";
|
||||
import { Typography } from "antd";
|
||||
import {
|
||||
LinkProps,
|
||||
useHref,
|
||||
useLinkClickHandler,
|
||||
} from "react-router-dom";
|
||||
import { BaseType } from "antd/es/typography/Base";
|
||||
|
||||
const Link = React.forwardRef(
|
||||
(
|
||||
{
|
||||
onClick,
|
||||
replace = false,
|
||||
state,
|
||||
target,
|
||||
to,
|
||||
type,
|
||||
...rest
|
||||
}: LinkProps,
|
||||
ref
|
||||
) => {
|
||||
let href = useHref(to);
|
||||
let handleClick = useLinkClickHandler(to, {
|
||||
replace,
|
||||
state,
|
||||
target,
|
||||
}) as (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
||||
|
||||
return (
|
||||
<Typography.Link
|
||||
{...rest}
|
||||
href={href}
|
||||
onClick={(event) => {
|
||||
onClick?.(event as React.MouseEvent<HTMLAnchorElement, MouseEvent>);
|
||||
if (!event.defaultPrevented) {
|
||||
handleClick(event);
|
||||
}
|
||||
}}
|
||||
ref={ref as React.RefObject<HTMLElement> | null}
|
||||
target={target}
|
||||
type={type as BaseType}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
export default Link;
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { Fragment, memo, useCallback, useEffect, useContext, useRef, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import {
|
||||
Button, Card, Checkbox, Col, ConfigProvider, Descriptions, Form, Input, Pagination,
|
||||
Button, Card, ColorPicker, Checkbox, Col, ConfigProvider, Descriptions, Form, Input, Pagination,
|
||||
Popconfirm, Row, Table, Tag, Typography, Select, Skeleton, Space, Spin, Switch, Modal, Tooltip,
|
||||
TablePaginationConfig,
|
||||
} from "antd";
|
||||
@@ -202,7 +202,7 @@ function SearchModsTable(props: SearchModsTableProps) {
|
||||
align: "right",
|
||||
render: (_, result) => <Select
|
||||
size="small"
|
||||
bordered={false}
|
||||
variant="borderless"
|
||||
value={modResultSelectedVersion.get(result.name) || 0}
|
||||
onChange={index => {
|
||||
const newVersions = new Map(modResultSelectedVersion);
|
||||
@@ -404,7 +404,7 @@ function hasChange(changes: ModChange[], field:Partial<ModChange>) {
|
||||
}
|
||||
|
||||
// picks out the value property from settings.
|
||||
function pickValue(map: Map<string, lib.ModSetting>): [string, string|number|boolean][] {
|
||||
function pickValue(map: Map<string, lib.ModSetting>): [string, lib.ModSetting["value"]][] {
|
||||
return [...map].map(([name, value]) => [name, value.value]);
|
||||
}
|
||||
|
||||
@@ -420,6 +420,25 @@ function groupToMap(array: any[], fn: (...args:any) => any) {
|
||||
return map;
|
||||
}
|
||||
|
||||
function InputColor(props: {
|
||||
value: lib.ModSettingColor,
|
||||
onChange: (value: lib.ModSettingColor) => void,
|
||||
}) {
|
||||
const value = props.value;
|
||||
return <ColorPicker
|
||||
defaultFormat="rgb"
|
||||
defaultValue={`rgba(${value.r * 255}, ${value.g * 255}, ${value.b * 255}, ${value.a}`}
|
||||
onChangeComplete={(color) => {
|
||||
const rgb = color.toRgb();
|
||||
props.onChange({
|
||||
r: rgb.r / 255,
|
||||
g: rgb.g / 255,
|
||||
b: rgb.b / 255,
|
||||
a: rgb.a,
|
||||
});
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
type SettingsTableFieldProps = {
|
||||
name: string;
|
||||
type: string;
|
||||
@@ -431,11 +450,15 @@ type SettingsTableFieldProps = {
|
||||
const SettingsTableField = memo((props: SettingsTableFieldProps) => {
|
||||
const name = props.name;
|
||||
const isBoolean = props.type === "bool-setting";
|
||||
const isColor = props.type === "color-setting";
|
||||
const isNumber = ["int-setting", "double-setting"].includes(props.type);
|
||||
const isSelect = Boolean(props.allowedValues);
|
||||
let input;
|
||||
if (isBoolean) {
|
||||
input = <Checkbox />;
|
||||
} else if (isColor) {
|
||||
const PartialInputColor = InputColor as () => React.JSX.Element;
|
||||
input = <PartialInputColor />;
|
||||
} else if (isSelect) {
|
||||
const options = Object.values(props.allowedValues).map(value => ({
|
||||
value,
|
||||
@@ -484,7 +507,7 @@ function SettingsTable(props: SettingsTableProps) {
|
||||
.map(mod => [mod.name, mod.title])
|
||||
);
|
||||
|
||||
const types = ["bool-setting", "int-setting", "double-setting", "string-setting"];
|
||||
const types = ["bool-setting", "int-setting", "double-setting", "string-setting", "color-setting"];
|
||||
let prototypes = Object.entries(props.prototypes)
|
||||
.filter(([type, _]) => types.includes(type))
|
||||
.flatMap<any>(([_, settingPrototypes]: [string, any]) => Object.values(settingPrototypes))
|
||||
@@ -492,7 +515,7 @@ function SettingsTable(props: SettingsTableProps) {
|
||||
|
||||
function controls(
|
||||
scope: "startup"|"runtime-global"|"runtime-per-user",
|
||||
storedFields: [string, string|number|boolean][]
|
||||
storedFields: [string, lib.ModSetting["value"]][]
|
||||
) {
|
||||
let fields = new Map(prototypes
|
||||
.filter(p => p.setting_type === scope)
|
||||
@@ -508,6 +531,8 @@ function SettingsTable(props: SettingsTableProps) {
|
||||
type = "double-setting";
|
||||
} else if (typeof value === "string") {
|
||||
type = "string-setting";
|
||||
} else if (typeof value === "object" && "r" in value) {
|
||||
type = "color-setting";
|
||||
} else {
|
||||
throw Error(`Unhandled setting type ${typeof value}`);
|
||||
}
|
||||
@@ -776,7 +801,7 @@ export default function ModPackViewPage() {
|
||||
title={modPack.name}
|
||||
extra={<Space>
|
||||
<ExportButton modPack={modifiedModPack}/>
|
||||
{account.hasPermission("core.mod-pack.delete") && <Popconfirm
|
||||
{account.hasPermission("core.mod_pack.delete") && <Popconfirm
|
||||
title="Delete mod pack and all of its settings?"
|
||||
placement="bottomRight"
|
||||
okText="Delete"
|
||||
|
||||
@@ -186,8 +186,8 @@ export default function ModsPage() {
|
||||
<SectionHeader
|
||||
title="Mod Packs"
|
||||
extra={<Space>
|
||||
{account.hasPermission("core.mod-pack.create") && <ImportModPackButton />}
|
||||
{account.hasPermission("core.mod-pack.create") && <CreateModPackButton />}
|
||||
{account.hasPermission("core.mod_pack.create") && <ImportModPackButton />}
|
||||
{account.hasPermission("core.mod_pack.create") && <CreateModPackButton />}
|
||||
</Space>}
|
||||
/>
|
||||
<Table
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Layout, Breadcrumb } from "antd";
|
||||
import Link from "./Link";
|
||||
|
||||
const { Content } = Layout;
|
||||
|
||||
|
||||
@@ -207,6 +207,7 @@ export default function SavesList(props: { instance: lib.InstanceDetails }) {
|
||||
|
||||
let hostOffline = ["unassigned", "unknown"].includes(props.instance.status!);
|
||||
const saveTable = <Table
|
||||
className="save-list-table"
|
||||
size="small"
|
||||
columns={[
|
||||
{
|
||||
@@ -236,7 +237,6 @@ export default function SavesList(props: { instance: lib.InstanceDetails }) {
|
||||
]}
|
||||
dataSource={[...saves.values()]}
|
||||
rowKey={save => save.name}
|
||||
pagination={false}
|
||||
expandable={{
|
||||
columnWidth: 33,
|
||||
expandRowByClick: true,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import React, { Fragment, useContext, useEffect, useState } from "react";
|
||||
import { Route, Routes, useNavigate } from "react-router-dom";
|
||||
import { Dropdown, Layout, Menu, MenuProps } from "antd";
|
||||
import UserOutlined from "@ant-design/icons/UserOutlined";
|
||||
@@ -115,12 +115,12 @@ export default function SiteLayout() {
|
||||
{combinedPages.map(({path, sidebarPath, content}) => <Route
|
||||
path={path}
|
||||
key={path}
|
||||
element={<>
|
||||
element={<Fragment key={path}>
|
||||
<SetSidebar path={sidebarPath ? sidebarPath : path} />
|
||||
<ErrorBoundary Component={ErrorPage}>
|
||||
{content}
|
||||
</ErrorBoundary>
|
||||
</>}
|
||||
</Fragment>}
|
||||
/>)}
|
||||
<Route element={<SetSidebar path={null} />} />
|
||||
</Routes>
|
||||
|
||||
@@ -88,7 +88,6 @@ export default function UserViewPage() {
|
||||
onChange={() => setRolesDirty(true)}
|
||||
loading={!roles}
|
||||
mode="multiple"
|
||||
showArrow={true}
|
||||
filterOption={(inputValue:string, option: any): boolean => {
|
||||
let role = roles.get(option.value);
|
||||
return role?.name.toLowerCase().includes(inputValue.toLowerCase()) ?? false;
|
||||
|
||||
@@ -12,6 +12,7 @@ import PageHeader from "./PageHeader";
|
||||
import PageLayout from "./PageLayout";
|
||||
import PluginExtra from "./PluginExtra";
|
||||
import { formatFirstSeen, formatLastSeen, sortFirstSeen, sortLastSeen, useUsers } from "../model/user";
|
||||
import Link from "./Link";
|
||||
|
||||
const strcmp = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }).compare;
|
||||
|
||||
@@ -49,7 +50,7 @@ function CreateUserButton() {
|
||||
>
|
||||
<Form form={form}>
|
||||
<Form.Item name="userName" label="Name">
|
||||
<Input/>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
@@ -97,7 +98,9 @@ export default function UsersPage() {
|
||||
title: "Roles",
|
||||
key: "roles",
|
||||
render: (_, user) => (
|
||||
[...user.roleIds].map(id => <Tag key={id}>{(roles.get(id) || { name: id }).name}</Tag>)
|
||||
[...user.roleIds].map(id => <Link to={`/roles/${id}/view`} onClick={e => e.stopPropagation()}>
|
||||
<Tag key={id}>{(roles.get(id) || { name: id }).name}</Tag>
|
||||
</Link>)
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -28,3 +28,5 @@ export { default as PageHeader } from "./components/PageHeader";
|
||||
export { default as PageLayout } from "./components/PageLayout";
|
||||
export { default as SavesList } from "./components/SavesList";
|
||||
export { default as SectionHeader } from "./components/SectionHeader";
|
||||
|
||||
export { default as Link } from "./components/Link";
|
||||
@@ -22,6 +22,12 @@ export function useAccount(): UserAccount {
|
||||
};
|
||||
}, [control]);
|
||||
|
||||
function checkPermissionExists(permission: string) {
|
||||
if (!lib.permissions.has(permission)) {
|
||||
throw new Error(`permission ${permission} does not exist`);
|
||||
}
|
||||
}
|
||||
|
||||
function hasPermission(permission: string): boolean {
|
||||
for (let role of roles) {
|
||||
if (role.permissions.includes("core.admin") || role.permissions.includes(permission)) {
|
||||
@@ -38,12 +44,16 @@ export function useAccount(): UserAccount {
|
||||
if (!roles) {
|
||||
return null;
|
||||
}
|
||||
checkPermissionExists(permission);
|
||||
return hasPermission(permission);
|
||||
},
|
||||
hasAnyPermission(...permissions: string[]): boolean | null {
|
||||
if (!roles) {
|
||||
return null;
|
||||
}
|
||||
for (let permission of permissions) {
|
||||
checkPermissionExists(permission);
|
||||
}
|
||||
for (let permission of permissions) {
|
||||
if (hasPermission(permission)) {
|
||||
return true;
|
||||
@@ -55,6 +65,9 @@ export function useAccount(): UserAccount {
|
||||
if (!roles) {
|
||||
return null;
|
||||
}
|
||||
for (let permission of permissions) {
|
||||
checkPermissionExists(permission);
|
||||
}
|
||||
for (let permission of permissions) {
|
||||
if (!hasPermission(permission)) {
|
||||
return false;
|
||||
|
||||
@@ -47,7 +47,7 @@ export const pages: PluginPage[] = [
|
||||
{
|
||||
path: "/mods",
|
||||
sidebarName: "Mods",
|
||||
permission: (account: UserAccount) => account.hasAnyPermission("core.mod.list", "core.mod-pack.list"),
|
||||
permission: (account: UserAccount) => account.hasAnyPermission("core.mod.list", "core.mod_pack.list"),
|
||||
content: <ModsPage />,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -182,10 +182,19 @@ body {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.save-list-dragger .ant-upload.ant-upload-drag {
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.save-list-dragger .ant-upload.ant-upload-btn {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.save-list-table .ant-table-pagination.ant-pagination {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.rcon-result code {
|
||||
display: block;
|
||||
background: #000;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.web.json",
|
||||
"extends": "../../tsconfig.browser.json",
|
||||
"references": [
|
||||
{ "path": "../lib/tsconfig.web.json" },
|
||||
{ "path": "../lib/tsconfig.browser.json" },
|
||||
],
|
||||
"include": [ "src/**/*.tsx", "src/**/*.ts", "package.json", "global.d.ts" ],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"outDir": "dist/browser",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.browser.json" },
|
||||
],
|
||||
}
|
||||
@@ -65,32 +65,47 @@ module.exports = (env = {}) => ({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
resolve: {
|
||||
// XXX Required due to bug in babel-runtime
|
||||
// see: https://github.com/babel/babel/issues/12058
|
||||
fullySpecified: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: require.resolve("ts-loader"),
|
||||
loader: require.resolve("swc-loader"),
|
||||
options: {
|
||||
configFile: "tsconfig.web.json",
|
||||
projectReferences: true,
|
||||
sourceMaps: true,
|
||||
jsc: {
|
||||
parser: {
|
||||
syntax: "typescript",
|
||||
tsx: true,
|
||||
},
|
||||
target: "es2022",
|
||||
transform: {
|
||||
react: {
|
||||
runtime: "automatic",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.jsx$/,
|
||||
test: /\.jsx?$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: require.resolve("babel-loader"),
|
||||
loader: require.resolve("swc-loader"),
|
||||
options: {
|
||||
presets: [require.resolve("@babel/preset-react")],
|
||||
sourceMaps: true,
|
||||
jsc: {
|
||||
parser: {
|
||||
syntax: "ecmascript",
|
||||
jsx: true,
|
||||
},
|
||||
target: "es2022",
|
||||
transform: {
|
||||
react: {
|
||||
runtime: "automatic",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -123,10 +138,10 @@ module.exports = (env = {}) => ({
|
||||
moduleIds: "deterministic",
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
minify: TerserPlugin.swcMinify,
|
||||
terserOptions: {
|
||||
compress: {
|
||||
keep_classnames: true,
|
||||
passes: 2,
|
||||
},
|
||||
mangle: {
|
||||
keep_classnames: true,
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
webpack.config.js
|
||||
dist/web/build
|
||||
dist/browser
|
||||
@@ -6,8 +6,8 @@ export const plugin: lib.PluginDeclaration = {
|
||||
name: "global_chat",
|
||||
title: "Global Chat",
|
||||
description: "Forwards chat between instances.",
|
||||
instanceEntrypoint: "dist/plugin/instance",
|
||||
ctlEntrypoint: "dist/plugin/control",
|
||||
instanceEntrypoint: "dist/node/instance",
|
||||
ctlEntrypoint: "dist/node/control",
|
||||
|
||||
messages: [
|
||||
ChatEvent,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@clusterio/plugin-global_chat",
|
||||
"version": "2.0.0-alpha.14",
|
||||
"version": "2.0.0-alpha.17",
|
||||
"description": "Clusterio plugin forwarding between Factorio servers",
|
||||
"author": "Hornwitser <github@hornwitser.no>",
|
||||
"homepage": "https://github.com/clusterio/clusterio#readme",
|
||||
"license": "MIT",
|
||||
"repository": "clusterio/clusterio",
|
||||
"main": "dist/plugin/index.js",
|
||||
"main": "dist/node/index.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build && webpack-cli --env production",
|
||||
"test": "echo \"Error: run tests from root\" && exit 1"
|
||||
@@ -21,8 +21,8 @@
|
||||
"@clusterio/lib": "^2.0.0-alpha.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@clusterio/lib": "^2.0.0-alpha.14",
|
||||
"@clusterio/web_ui": "^2.0.0-alpha.14",
|
||||
"@clusterio/lib": "^2.0.0-alpha.17",
|
||||
"@clusterio/web_ui": "^2.0.0-alpha.17",
|
||||
"@types/node": "^20.4.5",
|
||||
"@types/react": "^18.2.21",
|
||||
"typescript": "^5.1.6",
|
||||
|
||||
@@ -4,9 +4,9 @@ const lib = require("@clusterio/lib");
|
||||
|
||||
const mock = require("../../../test/mock");
|
||||
const lines = require("../../../test/lib/factorio/lines");
|
||||
const instance = require("../dist/plugin/instance");
|
||||
const info = require("../dist/plugin/index").plugin;
|
||||
const { ChatEvent } = require("../dist/plugin/messages");
|
||||
const instance = require("../dist/node/instance");
|
||||
const info = require("../dist/node/index").plugin;
|
||||
const { ChatEvent } = require("../dist/node/messages");
|
||||
|
||||
|
||||
describe("global_chat plugin", function() {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.browser.json",
|
||||
"references": [
|
||||
{ "path": "../../packages/lib/tsconfig.browser.json" },
|
||||
{ "path": "../../packages/web_ui/tsconfig.browser.json" },
|
||||
],
|
||||
"include": [ "web/**/*.tsx", "web/**/*.ts", "messages.ts", "package.json" ],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/browser",
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.node.json",
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "../../packages/lib" },
|
||||
{ "path": "./tsconfig.node.json" },
|
||||
{ "path": "./tsconfig.browser.json" },
|
||||
],
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": ["test/*", "./dist/*"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/plugin",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.node.json",
|
||||
"references": [
|
||||
{ "path": "../../packages/lib/tsconfig.node.json" },
|
||||
],
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": ["test/*", "./dist/*"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/node",
|
||||
},
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.web.json",
|
||||
"references": [
|
||||
{ "path": "../../packages/lib/tsconfig.web.json" },
|
||||
{ "path": "../../packages/web_ui/tsconfig.web.json" },
|
||||
],
|
||||
"include": [ "web/**/*.tsx", "web/**/*.ts", "messages.ts", "package.json" ],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/web/build",
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
webpack.config.js
|
||||
dist/web/build
|
||||
dist/browser
|
||||
@@ -22,7 +22,7 @@ export const plugin: lib.PluginDeclaration = {
|
||||
title: "Inventory sync",
|
||||
description: "Synchronizes players inventories between instances",
|
||||
|
||||
instanceEntrypoint: "dist/plugin/instance",
|
||||
instanceEntrypoint: "dist/node/instance",
|
||||
instanceConfigFields: {
|
||||
"inventory_sync.rcon_chunk_size": {
|
||||
title: "Rcon inventory chunk size",
|
||||
@@ -33,7 +33,7 @@ export const plugin: lib.PluginDeclaration = {
|
||||
},
|
||||
},
|
||||
|
||||
controllerEntrypoint: "dist/plugin/controller",
|
||||
controllerEntrypoint: "dist/node/controller",
|
||||
controllerConfigFields: {
|
||||
"inventory_sync.player_lock_timeout": {
|
||||
title: "Player Lock Timeout",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@clusterio/plugin-inventory_sync",
|
||||
"version": "2.0.0-alpha.14",
|
||||
"version": "2.0.0-alpha.17",
|
||||
"description": "Clusterio plugin forwarding between Factorio servers",
|
||||
"author": "Danielv <danielv@danielv.no>",
|
||||
"homepage": "https://github.com/clusterio/factorioClusterio#readme",
|
||||
"license": "MIT",
|
||||
"repository": "clusterio/factorioClusterio",
|
||||
"main": "dist/plugin/index.js",
|
||||
"main": "dist/node/index.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build && webpack-cli --env production",
|
||||
"test": "echo \"Error: run tests from root\" && exit 1"
|
||||
@@ -25,8 +25,8 @@
|
||||
"fs-extra": "^8.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@clusterio/lib": "^2.0.0-alpha.14",
|
||||
"@clusterio/web_ui": "^2.0.0-alpha.14",
|
||||
"@clusterio/lib": "^2.0.0-alpha.17",
|
||||
"@clusterio/web_ui": "^2.0.0-alpha.17",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/node": "^20.4.5",
|
||||
"@types/react": "^18.2.21",
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.browser.json",
|
||||
"references": [
|
||||
{ "path": "../../packages/lib/tsconfig.browser.json" },
|
||||
{ "path": "../../packages/web_ui/tsconfig.browser.json" },
|
||||
],
|
||||
"include": [ "web/**/*.tsx", "web/**/*.ts", "messages.ts", "package.json" ],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/browser",
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.node.json",
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "../../packages/lib" },
|
||||
{ "path": "./tsconfig.node.json" },
|
||||
{ "path": "./tsconfig.browser.json" },
|
||||
],
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": ["test/*", "./dist/*"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/plugin",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.node.json",
|
||||
"references": [
|
||||
{ "path": "../../packages/lib/tsconfig.node.json" },
|
||||
],
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": ["test/*", "./dist/*"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/node",
|
||||
},
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.web.json",
|
||||
"references": [
|
||||
{ "path": "../../packages/lib/tsconfig.web.json" },
|
||||
{ "path": "../../packages/web_ui/tsconfig.web.json" },
|
||||
],
|
||||
"include": [ "web/**/*.tsx", "web/**/*.ts", "messages.ts", "package.json" ],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/web/build",
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
webpack.config.js
|
||||
dist/web/build
|
||||
dist/browser
|
||||
@@ -15,8 +15,8 @@ export const plugin: lib.PluginDeclaration = {
|
||||
name: "player_auth",
|
||||
title: "Player Auth",
|
||||
description: "Provides authentication to the cluster via logging into a Factorio server.",
|
||||
controllerEntrypoint: "dist/plugin/controller",
|
||||
instanceEntrypoint: "dist/plugin/instance",
|
||||
controllerEntrypoint: "dist/node/controller",
|
||||
instanceEntrypoint: "dist/node/instance",
|
||||
webEntrypoint: "./web",
|
||||
controllerConfigFields: {
|
||||
"player_auth.code_length": {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@clusterio/plugin-player_auth",
|
||||
"version": "2.0.0-alpha.14",
|
||||
"version": "2.0.0-alpha.17",
|
||||
"description": "Clusterio plugin authenticating logged in players to the web interface",
|
||||
"author": "Hornwitser <github@hornwitser.no>",
|
||||
"homepage": "https://github.com/clusterio/clusterio#readme",
|
||||
"license": "MIT",
|
||||
"repository": "clusterio/clusterio",
|
||||
"main": "dist/plugin/index.js",
|
||||
"main": "dist/node/index.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build && webpack-cli --env production",
|
||||
"test": "echo \"Error: run tests from root\" && exit 1"
|
||||
@@ -26,8 +26,8 @@
|
||||
"jsonwebtoken": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@clusterio/lib": "^2.0.0-alpha.14",
|
||||
"@clusterio/web_ui": "^2.0.0-alpha.14",
|
||||
"@clusterio/lib": "^2.0.0-alpha.17",
|
||||
"@clusterio/web_ui": "^2.0.0-alpha.17",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jsonwebtoken": "^9.0.2",
|
||||
"@types/node": "^20.4.5",
|
||||
|
||||
@@ -5,10 +5,10 @@ const jwt = require("jsonwebtoken");
|
||||
|
||||
const mock = require("../../../test/mock");
|
||||
|
||||
const controller = require("../dist/plugin/controller");
|
||||
const instance = require("../dist/plugin/instance");
|
||||
const info = require("../dist/plugin/index").plugin;
|
||||
const { FetchPlayerCodeRequest, SetVerifyCodeRequest } = require("../dist/plugin/messages");
|
||||
const controller = require("../dist/node/controller");
|
||||
const instance = require("../dist/node/instance");
|
||||
const info = require("../dist/node/index").plugin;
|
||||
const { FetchPlayerCodeRequest, SetVerifyCodeRequest } = require("../dist/node/messages");
|
||||
const lib = require("@clusterio/lib");
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.browser.json",
|
||||
"references": [
|
||||
{ "path": "../../packages/lib/tsconfig.browser.json" },
|
||||
{ "path": "../../packages/web_ui/tsconfig.browser.json" },
|
||||
],
|
||||
"include": [ "web/**/*.tsx", "web/**/*.ts", "messages.ts", "package.json" ],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/browser",
|
||||
}
|
||||
}
|
||||
Loaded 100 of 150 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user