diff --git a/packages/lib/src/database.ts b/packages/lib/src/database.ts index 3b388ecf..3f54c301 100644 --- a/packages/lib/src/database.ts +++ b/packages/lib/src/database.ts @@ -224,14 +224,14 @@ export class ItemDatabase { * @param name - The name of the item to get the count of. * @returns The count of the item stored. */ - getItemCount(name: string) { + getItemCount(name: string): number { checkName(name); if (!this._items.has(name)) { return 0; } - return this._items.get(name); + return this._items.get(name)!; } /** @@ -274,4 +274,11 @@ export class ItemDatabase { this._items.set(name, count); }; + + /** + * Allow to iterate through the items. + */ + getEntries(): IterableIterator<[string, number]> { + return this._items.entries() + } } diff --git a/packages/lib/src/plugin.ts b/packages/lib/src/plugin.ts index de231701..4d2b86a9 100644 --- a/packages/lib/src/plugin.ts +++ b/packages/lib/src/plugin.ts @@ -10,6 +10,9 @@ import type { ParsedFactorioOutput } from "./logging_utils"; import type { ModPack, ModInfo } from "./data"; import type { Link } from "./link"; import type { CommandTree } from "./command"; +import type { User } from "./users"; +import type { MessageRequest, MessageEvent } from "./data"; + /** * Conceptual base for controller and instance plugins. @@ -671,6 +674,10 @@ export interface PluginPage { * Should render a PageLayout. */ content?: React["ReactNode"]; + /** + * Permission to access page. function are expected to throw an error if access is deny. + */ + permission?: string | ((user: User, message: MessageRequest|MessageEvent) => void); }; /** diff --git a/plugins/global_chat/messages.ts b/plugins/global_chat/messages.ts index a7e24e68..5dd20696 100644 --- a/plugins/global_chat/messages.ts +++ b/plugins/global_chat/messages.ts @@ -1,9 +1,10 @@ import { Type, Static } from "@sinclair/typebox"; export class ChatEvent { - static type = "event"; - static src = ["control", "instance"]; - static dst = "instance"; - static plugin = "global_chat"; + declare ["constructor"]: typeof ChatEvent; + static type = "event" as const; + static src = ["control", "instance"] as const; + static dst = "instance" as const; + static plugin = "global_chat" as const; static permission = null; constructor( diff --git a/plugins/inventory_sync/controller.js b/plugins/inventory_sync/controller.ts similarity index 75% rename from plugins/inventory_sync/controller.js rename to plugins/inventory_sync/controller.ts index ee7834a1..6992903d 100644 --- a/plugins/inventory_sync/controller.js +++ b/plugins/inventory_sync/controller.ts @@ -1,25 +1,20 @@ -"use strict"; -const fs = require("fs-extra"); -const path = require("path"); +import type InstanceInfo from "@clusterio/controller/src/InstanceInfo"; +import type Controller from "@clusterio/controller/src/Controller"; +import type { IpcPlayerData } from "./instance"; -const lib = require("@clusterio/lib"); +import fs from "fs-extra"; +import path from "path"; +import * as lib from "@clusterio/lib"; +import * as msg from "./messages"; -const { - AcquireRequest, - ReleaseRequest, - UploadRequest, - DownloadRequest, - DatabaseStatsRequest, -} = require("./messages"); - -async function loadDatabase(config, logger) { +async function loadDatabase(config: lib.ControllerConfig, logger: lib.Logger) { let itemsPath = path.resolve(config.get("controller.database_directory"), "inventories.json"); logger.verbose(`Loading ${itemsPath}`); try { - let content = await fs.readFile(itemsPath); + let content = await fs.readFile(itemsPath, { encoding: "utf8" }); return new Map(JSON.parse(content)); - } catch (err) { + } catch (err: any) { if (err.code === "ENOENT") { logger.verbose("Creating new player data database"); return new Map(); @@ -28,7 +23,11 @@ async function loadDatabase(config, logger) { } } -async function saveDatabase(controllerConfig, playerDatastore, logger) { +async function saveDatabase( + controllerConfig: lib.ControllerConfig, + playerDatastore: Map, + logger: lib.Logger, +) { if (playerDatastore) { let file = path.resolve(controllerConfig.get("controller.database_directory"), "inventories.json"); logger.verbose(`writing ${file}`); @@ -37,7 +36,11 @@ async function saveDatabase(controllerConfig, playerDatastore, logger) { } } -class ControllerPlugin extends lib.BaseControllerPlugin { +export class ControllerPlugin extends lib.BaseControllerPlugin { + acquiredPlayers!: Map; + playerDatastore!: Map; + autosaveId!: ReturnType; + async init() { this.acquiredPlayers = new Map(); this.playerDatastore = await loadDatabase(this.controller.config, this.logger); @@ -47,14 +50,14 @@ class ControllerPlugin extends lib.BaseControllerPlugin { }); }, this.controller.config.get("inventory_sync.autosave_interval") * 1000); - this.controller.handle(AcquireRequest, this.handleAcquireRequest.bind(this)); - this.controller.handle(ReleaseRequest, this.handleReleaseRequest.bind(this)); - this.controller.handle(UploadRequest, this.handleUploadRequest.bind(this)); - this.controller.handle(DownloadRequest, this.handleDownloadRequest.bind(this)); - this.controller.handle(DatabaseStatsRequest, this.handleDatabaseStatsRequest.bind(this)); + this.controller.handle(msg.AcquireRequest, this.handleAcquireRequest.bind(this)); + this.controller.handle(msg.ReleaseRequest, this.handleReleaseRequest.bind(this)); + this.controller.handle(msg.UploadRequest, this.handleUploadRequest.bind(this)); + this.controller.handle(msg.DownloadRequest, this.handleDownloadRequest.bind(this)); + this.controller.handle(msg.DatabaseStatsRequest, this.handleDatabaseStatsRequest.bind(this)); } - async onInstanceStatusChanged(instance) { + async onInstanceStatusChanged(instance: InstanceInfo) { let instanceId = instance.id; if (["unassigned", "deleted"].includes(instance.status)) { for (let [playerName, acquisitionRecord] of this.acquiredPlayers) { @@ -82,7 +85,7 @@ class ControllerPlugin extends lib.BaseControllerPlugin { } } - acquire(instanceId, playerName) { + acquire(instanceId: number, playerName: string): boolean { let acquisitionRecord = this.acquiredPlayers.get(playerName); if ( !acquisitionRecord @@ -97,11 +100,11 @@ class ControllerPlugin extends lib.BaseControllerPlugin { return false; } - async handleAcquireRequest(request) { + async handleAcquireRequest(request: msg.AcquireRequest) { let { instanceId, playerName } = request; if (!this.acquire(instanceId, playerName)) { let acquisitionRecord = this.acquiredPlayers.get(playerName); - let instance = this.controller.instances.get(acquisitionRecord.instanceId); + let instance = this.controller.instances.get(acquisitionRecord!.instanceId); return { status: "busy", message: instance.config.get("instance.name"), @@ -109,14 +112,14 @@ class ControllerPlugin extends lib.BaseControllerPlugin { } let playerData = this.playerDatastore.get(playerName); - return new AcquireRequest.Response( + return new msg.AcquireRequest.Response( "acquired", playerData ? playerData.generation : 0, Boolean(playerData), ); } - async handleReleaseRequest(request) { + async handleReleaseRequest(request: msg.ReleaseRequest) { let { instanceId, playerName } = request; let acquisitionRecord = this.acquiredPlayers.get(playerName); if (!acquisitionRecord) { @@ -128,7 +131,7 @@ class ControllerPlugin extends lib.BaseControllerPlugin { } } - async handleUploadRequest(request) { + async handleUploadRequest(request: msg.UploadRequest) { let { instanceId, playerName, playerData } = request; let instanceName = this.controller.instances.get(instanceId).config.get("instance.name"); let store = true; @@ -162,7 +165,7 @@ class ControllerPlugin extends lib.BaseControllerPlugin { } } - async handleDownloadRequest(request) { + async handleDownloadRequest(request: msg.DownloadRequest) { let { instanceId, playerName } = request; let instanceName = this.controller.instances.get(instanceId).config.get("instance.name"); @@ -174,7 +177,7 @@ class ControllerPlugin extends lib.BaseControllerPlugin { } this.logger.verbose(`Sending player data for ${playerName} to ${instanceName}`); - return new DownloadRequest.Response(this.playerDatastore.get(playerName) || null); + return new msg.DownloadRequest.Response(this.playerDatastore.get(playerName) || null); } async onShutdown() { @@ -189,7 +192,7 @@ class ControllerPlugin extends lib.BaseControllerPlugin { length: JSON.stringify(this.playerDatastore.get(name)).length, })) .sort((a, b) => b.length - a.length); - return new DatabaseStatsRequest.Response( + return new msg.DatabaseStatsRequest.Response( playerDatastore.map(x => x.length).reduce((a, b) => b - a, 0), playerDatastore.length, { @@ -199,7 +202,3 @@ class ControllerPlugin extends lib.BaseControllerPlugin { ); } } - -module.exports = { - ControllerPlugin, -}; diff --git a/plugins/inventory_sync/info.js b/plugins/inventory_sync/info.ts similarity index 88% rename from plugins/inventory_sync/info.js rename to plugins/inventory_sync/info.ts index 627b9b41..53c005e8 100644 --- a/plugins/inventory_sync/info.js +++ b/plugins/inventory_sync/info.ts @@ -1,6 +1,5 @@ -"use strict"; -const lib = require("@clusterio/lib"); -let messages = require("./messages"); +import * as lib from "@clusterio/lib"; +import * as messages from "./messages"; class ControllerConfigGroup extends lib.PluginConfigGroup { } ControllerConfigGroup.defaultAccess = ["controller", "host", "control"]; @@ -41,15 +40,15 @@ lib.definePermission({ grantByDefault: true, }); -module.exports.default = { +const info: lib.PluginInfo = { name: "inventory_sync", title: "Inventory sync", description: "Synchronizes players inventories between instances", - instanceEntrypoint: "instance", + instanceEntrypoint: "dist/plugin/instance", InstanceConfigGroup, - controllerEntrypoint: "controller", + controllerEntrypoint: "dist/plugin/controller", ControllerConfigGroup, messages: [ @@ -61,4 +60,6 @@ module.exports.default = { ], webEntrypoint: "./web", routes: ["/inventory"], -}; +} + +export default info; \ No newline at end of file diff --git a/plugins/inventory_sync/instance.js b/plugins/inventory_sync/instance.ts similarity index 65% rename from plugins/inventory_sync/instance.js rename to plugins/inventory_sync/instance.ts index 9a696a71..c460e339 100644 --- a/plugins/inventory_sync/instance.js +++ b/plugins/inventory_sync/instance.ts @@ -1,47 +1,61 @@ -/** - * @module - */ -"use strict"; -const lib = require("@clusterio/lib"); +import * as lib from "@clusterio/lib"; +import { AcquireRequest, ReleaseRequest, UploadRequest, DownloadRequest } from "./messages"; -const { - AcquireRequest, - ReleaseRequest, - UploadRequest, - DownloadRequest, -} = require("./messages"); +type IpcPlayerName = { + player_name: string +} +export type IpcPlayerData = { //.\module\serialize.lua:serialize.serialize_player() + generation: number, + controller: string, + name: string, + color: number[], + chat_color: number[], + tag: string, + force: string, + cheat_mode: boolean, + flashlight: boolean, + ticks_to_respawn?: number, + character?: any, + inventories?: any, + hotbar?: string[], + personal_logistic_slots?: {name:string, min:number, max:number}[], + crafting_queue?: any, +} /** * Splits string into array of strings with max of a certain length - * @param {Number} chunkSize - Max length of each chunk - * @param {String} string - String to split into chunks - * @returns {String[]} Chunks + * @param chunkSize - Max length of each chunk + * @param string - String to split into chunks */ -function chunkify(chunkSize, string) { - return string.match(new RegExp(`.{1,${chunkSize}}`, "g")); +function chunkify(chunkSize: number, string: string): string[] { + return string.match(new RegExp(`.{1,${chunkSize}}`, "g")) || []; } -class InstancePlugin extends lib.BaseInstancePlugin { +export class InstancePlugin extends lib.BaseInstancePlugin { + playersToRelease!: Set; + disconnecting!: boolean; + async init() { this.playersToRelease = new Set(); this.disconnecting = false; // Handle IPC from scenario script - this.instance.server.on("ipc-inventory_sync_acquire", content => this.handleAcquire(content) + this.instance.server.on("ipc-inventory_sync_acquire", (request: IpcPlayerName) => this.handleAcquire(request) .catch(err => this.logger.error(`Error handling ipc-inventory_sync_acquire:\n${err.stack}`))); - this.instance.server.on("ipc-inventory_sync_release", content => this.handleRelease(content) + this.instance.server.on("ipc-inventory_sync_release", (request: IpcPlayerName) => this.handleRelease(request) .catch(err => this.logger.error(`Error handling ipc-inventory_sync_release:\n${err.stack}`))); - this.instance.server.on("ipc-inventory_sync_upload", content => this.handleUpload(content) + this.instance.server.on("ipc-inventory_sync_upload", (player_data: IpcPlayerData) => this.handleUpload(player_data) .catch(err => this.logger.error(`Error handling ipc-inventory_sync_upload:\n${err.stack}`))); - this.instance.server.on("ipc-inventory_sync_download", content => this.handleDownload(content) + this.instance.server.on("ipc-inventory_sync_download", (request: IpcPlayerName) => this.handleDownload(request) .catch(err => this.logger.error(`Error handling ipc-inventory_sync_download:\n${err.stack}`))); } - onPrepareControllerDisconnect() { + async onPrepareControllerDisconnect() { this.disconnecting = true; } - onControllerConnectionEvent(event) { + + onControllerConnectionEvent(event: "connect" | "drop" | "resume" | "close") { if (event === "connect") { this.disconnecting = false; (async () => { @@ -49,7 +63,7 @@ class InstancePlugin extends lib.BaseInstancePlugin { if (!this.host.connector.connected || this.disconnecting) { return; } - this.playersToRelease.delete(player); + this.playersToRelease.delete(player_name); await this.instance.sendTo("controller", new ReleaseRequest(this.instance.id, player_name)); } })().catch( @@ -58,11 +72,13 @@ class InstancePlugin extends lib.BaseInstancePlugin { } } - async handleAcquire(request) { + async handleAcquire(request: IpcPlayerName) { let response = { player_name: request.player_name, status: "error", message: "Controller is temporarily unavailable", + generation: undefined, + has_data: undefined, }; if (this.host.connector.connected && !this.disconnecting) { @@ -78,7 +94,7 @@ class InstancePlugin extends lib.BaseInstancePlugin { has_data: acquireResponse.hasData, message: acquireResponse.message, }; - } catch (err) { + } catch (err: any) { if (!(err instanceof lib.SessionLost)) { this.logger.error(`Unexpected error sending aquire request:\n${err.stack}`); response.message = err.message; @@ -90,23 +106,23 @@ class InstancePlugin extends lib.BaseInstancePlugin { await this.sendRcon(`/sc inventory_sync.acquire_response("${json}")`, true); } - async handleRelease(request) { + async handleRelease(request: IpcPlayerName) { if (!this.host.connector.connected) { - this.playersToRelease.set(request.player_name); + this.playersToRelease.add(request.player_name); } try { await this.instance.sendTo("controller", new ReleaseRequest(this.instance.id, request.player_name)); - } catch (err) { + } catch (err: any) { if (err instanceof lib.SessionLost) { - this.playersToRelease.set(request.player_name); + this.playersToRelease.add(request.player_name); } else { this.logger.error(`Unexpected error releasing player ${request.player_name}:\n${err.stack}`); } } } - async handleUpload(player_data) { + async handleUpload(player_data: IpcPlayerData) { if (!this.host.connector.connected || this.disconnecting) { return; } @@ -118,7 +134,7 @@ class InstancePlugin extends lib.BaseInstancePlugin { new UploadRequest(this.instance.id, player_data.name, player_data), ); - } catch (err) { + } catch (err: any) { if (!(err instanceof lib.SessionLost)) { this.logger.error(`Unexpected error uploading inventory for ${player_data.name}:\n${err.stack}`); } @@ -130,7 +146,7 @@ class InstancePlugin extends lib.BaseInstancePlugin { ); } - async handleDownload(request) { + async handleDownload(request: IpcPlayerName) { const playerName = request.player_name; this.logger.verbose(`Downloading ${playerName}`); @@ -154,7 +170,3 @@ class InstancePlugin extends lib.BaseInstancePlugin { } } } - -module.exports = { - InstancePlugin, -}; diff --git a/plugins/inventory_sync/messages.js b/plugins/inventory_sync/messages.js deleted file mode 100644 index f1e58e98..00000000 --- a/plugins/inventory_sync/messages.js +++ /dev/null @@ -1,229 +0,0 @@ -"use strict"; - - -class AcquireRequest { - static type = "request"; - static src = "instance"; - static dst = "controller"; - static plugin = "inventory_sync"; - - /** @type {number} */ - instanceId; - /** @type {string} */ - playerName; - - constructor(instanceId, playerName) { - this.instanceId = instanceId; - this.playerName = playerName; - } - - static jsonSchema = { - type: "object", - properties: { - "instanceId": { type: "integer" }, - "playerName": { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.instanceId, json.playerName); - } -} -AcquireRequest.Response = class Response { - /** @type {string} */ - status; - /** @type {number} */ - generation; - /** @type {boolean} */ - hasData; - /** @type {string} */ - message; - - constructor(status, generation, hasData, message) { - this.status = status; - if (generation !== undefined) { this.generation = generation; } - if (hasData !== undefined) { this.hasData = hasData; } - if (message !== undefined) { this.message = message; } - } - - static jsonSchema = { - type: "object", - required: ["status"], - properties: { - "status": { - type: "string", - enum: ["acquired", "error", "busy"], - }, - "generation": { type: "integer" }, - "hasData": { type: "boolean" }, - "message": { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.status, json.generation, json.hasData, json.message); - } -}; - -class ReleaseRequest { - static type = "request"; - static src = "instance"; - static dst = "controller"; - static plugin = "inventory_sync"; - - /** @type {boolean} */ - instanceId; - /** @type {boolean} */ - playerName; - - constructor(instanceId, playerName) { - this.instanceId = instanceId; - this.playerName = playerName; - } - - static jsonSchema = { - type: "object", - properties: { - "instanceId": { type: "integer" }, - "playerName": { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.instanceId, json.playerName); - } -} - -class UploadRequest { - static type = "request"; - static src = "instance"; - static dst = "controller"; - static plugin = "inventory_sync"; - - /** @type {boolean} */ - instanceId; - /** @type {boolean} */ - playerName; - /** @type {object} */ - playerData; - - constructor(instanceId, playerName, playerData) { - this.instanceId = instanceId; - this.playerName = playerName; - this.playerData = playerData; - } - - static jsonSchema = { - type: "object", - required: ["instanceId", "playerName", "playerData"], - properties: { - "instanceId": { type: "integer" }, - "playerName": { type: "string" }, - "playerData": { type: "object" }, - }, - }; - - static fromJSON(json) { - return new this(json.instanceId, json.playerName, json.playerData); - } -} - -class DownloadRequest { - static type = "request"; - static src = "instance"; - static dst = "controller"; - static plugin = "inventory_sync"; - - /** @type {number} */ - instanceId; - /** @type {string} */ - playerName; - - constructor(instanceId, playerName) { - this.instanceId = instanceId; - this.playerName = playerName; - } - - static jsonSchema = { - type: "object", - required: ["instanceId", "playerName"], - properties: { - "instanceId": { type: "integer" }, - "playerName": { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.instanceId, json.playerName); - } -} -DownloadRequest.Response = class Response { - /** @type {?object} */ - playerData; - - constructor(playerData) { - this.playerData = playerData; - } - - static jsonSchema = { - type: "object", - required: ["playerData"], - properties: { - "playerData": { type: ["object", "null"] }, - }, - }; - - static fromJSON(json) { - return new this(json.playerData); - } -}; - -class DatabaseStatsRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static plugin = "inventory_sync"; - static permission = "inventory_sync.inventory.view"; -} -DatabaseStatsRequest.Response = class Response { - /** @type {boolean} */ - databaseSize; - /** @type {boolean} */ - databaseEntries; - /** @type {object} */ - largestEntry; - - constructor(databaseSize, databaseEntries, largestEntry) { - this.databaseSize = databaseSize; - this.databaseEntries = databaseEntries; - this.largestEntry = largestEntry; - } - - static jsonSchema = { - type: "object", - required: ["databaseSize", "databaseEntries", "largestEntry"], - properties: { - "databaseSize": { type: "integer" }, - "databaseEntries": { type: "integer" }, - "largestEntry": { - type: "object", - properties: { - name: { type: "string" }, - size: { type: "number" }, - }, - }, - }, - }; - - static fromJSON(json) { - return new this(json.databaseSize, json.databaseEntries, json.largestEntry); - } -}; - -module.exports = { - AcquireRequest, - ReleaseRequest, - UploadRequest, - DownloadRequest, - DatabaseStatsRequest, -}; diff --git a/plugins/inventory_sync/messages.ts b/plugins/inventory_sync/messages.ts new file mode 100644 index 00000000..8eb7f390 --- /dev/null +++ b/plugins/inventory_sync/messages.ts @@ -0,0 +1,172 @@ +import type { IpcPlayerData } from "./instance"; + +import { Type, Static } from "@sinclair/typebox"; +import { StringEnum } from "@clusterio/lib"; + +const jsonPlayerData = Type.Object({ + generation: Type.Number(), + name: Type.String(), +}) + +class AcquireResponse { + constructor( + public status: string, + public generation?: number, + public hasData?: boolean, + public message?: string, + ) { + } + + static jsonSchema = Type.Object({ + "status": StringEnum(["acquired", "error", "busy"]), + "generation": Type.Optional(Type.Integer()), + "hasData": Type.Optional(Type.Boolean()), + "message": Type.Optional(Type.String()), + }); + + static fromJSON(json: Static): AcquireResponse { + return new this(json.status, json.generation, json.hasData, json.message); + } +}; +export class AcquireRequest { + declare ["constructor"]: typeof AcquireRequest; + static type = "request" as const; + static src = "instance" as const; + static dst = "controller" as const; + static plugin = "inventory_sync" as const; + static Response = AcquireResponse; + + constructor( + public instanceId: number, + public playerName: string, + ) { + } + + static jsonSchema = Type.Object({ + "instanceId": Type.Number(), + "playerName": Type.String(), + }) + + static fromJSON(json: Static): AcquireRequest { + return new this(json.instanceId, json.playerName); + } +} + +export class ReleaseRequest { + declare ["constructor"]: typeof ReleaseRequest; + static type = "request" as const; + static src = "instance" as const; + static dst = "controller" as const; + static plugin = "inventory_sync" as const; + + constructor( + public instanceId: number, + public playerName: string + ) { + } + + static jsonSchema = Type.Object({ + "instanceId": Type.Integer(), + "playerName": Type.String(), + }) + + static fromJSON(json: Static) { + return new this(json.instanceId, json.playerName); + } +} + +export class UploadRequest { + declare ["constructor"]: typeof UploadRequest; + static type = "request" as const; + static src = "instance" as const; + static dst = "controller" as const; + static plugin = "inventory_sync" as const; + + constructor( + public instanceId: number, + public playerName: string, + public playerData: IpcPlayerData, + ) { + } + + static jsonSchema = Type.Object({ + "instanceId": Type.Integer(), + "playerName": Type.String(), + "playerData": jsonPlayerData, + }); + + static fromJSON(json: Static) { + return new this(json.instanceId, json.playerName, json.playerData as IpcPlayerData); + } +} + +class DownloadResponse { + constructor( + public playerData: IpcPlayerData | null + ) { + } + + static jsonSchema = Type.Object({ + "playerData": Type.Optional(jsonPlayerData), + }) + + static fromJSON(json: Static): DownloadResponse { + return new this(json.playerData as IpcPlayerData); + } +} + +export class DownloadRequest { + declare ["constructor"]: typeof DownloadRequest; + static type = "request" as const; + static src = "instance" as const; + static dst = "controller" as const; + static plugin = "inventory_sync" as const; + static Response = DownloadResponse; + + constructor( + public instanceId: number, + public playerName: string, + ) { + } + + static jsonSchema = Type.Object({ + "instanceId": Type.Integer(), + "playerName": Type.String(), + }) + + static fromJSON(json: Static) { + return new this(json.instanceId, json.playerName); + } +} + +class DatabaseStatsResponse { + constructor( + public databaseSize: number, + public databaseEntries: number, + public largestEntry: { name: string, size: number }, + ) { + } + + static jsonSchema = Type.Object({ + "databaseSize": Type.Integer(), + "databaseEntries": Type.Integer(), + "largestEntry": Type.Object({ + "name": Type.String(), + "size": Type.Number(), + }) + }) + + static fromJSON(json: Static): DatabaseStatsResponse { + return new this(json.databaseSize, json.databaseEntries, json.largestEntry); + } +} + +export class DatabaseStatsRequest { + declare ["constructor"]: typeof DatabaseStatsRequest; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static plugin = "inventory_sync" as const; + static permission = "inventory_sync.inventory.view" as const; + static Response = DatabaseStatsResponse; +} diff --git a/plugins/inventory_sync/package.json b/plugins/inventory_sync/package.json index c47b1cab..d1d72515 100644 --- a/plugins/inventory_sync/package.json +++ b/plugins/inventory_sync/package.json @@ -6,9 +6,9 @@ "homepage": "https://github.com/clusterio/factorioClusterio#readme", "license": "MIT", "repository": "clusterio/factorioClusterio", - "main": "./info.js", + "main": "dist/plugin/info.js", "scripts": { - "prepare": "webpack-cli --env production", + "prepare": "tsc --build && webpack-cli --env production", "test": "echo \"Error: run tests from root\" && exit 1" }, "bugs": { @@ -21,14 +21,18 @@ "@clusterio/lib": "^2.0.0-alpha.0" }, "dependencies": { + "@sinclair/typebox": "^0.30.4", "fs-extra": "^8.1.0" }, "devDependencies": { "@clusterio/lib": "^2.0.0-alpha.13", "@clusterio/web_ui": "^2.0.0-alpha.13", + "@types/fs-extra": "^11.0.1", + "@types/node": "^20.4.5", "antd": "^5.7.3", "react": "^18.2.0", "react-dom": "^18.2.0", + "typescript": "^5.1.6", "webpack": "^5.88.2", "webpack-cli": "^5.1.4", "webpack-merge": "^5.9.0" diff --git a/plugins/inventory_sync/tsconfig.json b/plugins/inventory_sync/tsconfig.json new file mode 100644 index 00000000..5408f6b2 --- /dev/null +++ b/plugins/inventory_sync/tsconfig.json @@ -0,0 +1,16 @@ +{ + "include": ["./**/*.ts"], + "exclude": ["test/*", "./dist/*"], + "compilerOptions": { + "lib": ["es2023"], + "module": "commonjs", + "target": "es2022", + "rootDir": ".", + "outDir": "dist/plugin", + "sourceMap": true, + "strict": true, + "alwaysStrict": true, + "esModuleInterop": true, + "skipLibCheck": true + } +} diff --git a/plugins/inventory_sync/web/index.jsx b/plugins/inventory_sync/web/index.jsx index d3c9218f..a0af9acc 100644 --- a/plugins/inventory_sync/web/index.jsx +++ b/plugins/inventory_sync/web/index.jsx @@ -2,7 +2,7 @@ import React, { useContext, useEffect, useState } from "react"; import * as lib from "@clusterio/lib"; import { PageLayout, ControlContext } from "@clusterio/web_ui"; -import { DatabaseStatsRequest } from "../messages"; +import { DatabaseStatsRequest } from "../dist/plugin/messages"; import "./index.css"; diff --git a/plugins/inventory_sync/webpack.config.js b/plugins/inventory_sync/webpack.config.js index c2d7703f..857e098b 100644 --- a/plugins/inventory_sync/webpack.config.js +++ b/plugins/inventory_sync/webpack.config.js @@ -16,7 +16,7 @@ module.exports = (env = {}) => merge(common(env), { name: "inventory_sync", library: { type: "var", name: "plugin_inventory_sync" }, exposes: { - "./info": "./info.js", + "./info": "./dist/plugin/info.js", "./package.json": "./package.json", "./web": "./web/index.jsx", }, diff --git a/plugins/player_auth/controller.js b/plugins/player_auth/controller.ts similarity index 79% rename from plugins/player_auth/controller.js rename to plugins/player_auth/controller.ts index 03f27374..819a55a2 100644 --- a/plugins/player_auth/controller.js +++ b/plugins/player_auth/controller.ts @@ -1,19 +1,17 @@ -/** - * @module - */ -"use strict"; -const crypto = require("crypto"); -const express = require("express"); -const util = require("util"); -const jwt = require("jsonwebtoken"); +import type { Request, Response } from "express"; -const lib = require("@clusterio/lib"); +import crypto from "crypto"; +import express from "express"; +import util from "util"; +import jwt from "jsonwebtoken"; + +import * as lib from "@clusterio/lib"; const { basicType } = lib; -const { FetchPlayerCodeRequest, SetVerifyCodeRequest } = require("./messages"); +import { FetchPlayerCodeRequest, SetVerifyCodeRequest } from "./messages"; -async function generateCode(length) { +async function generateCode(length: number): Promise { // ji1lI, 0oOQ, and 2Z are not present to ease reading. let letters = "abcdefghkmnpqrstuvwxyzABCDEFGHJKLMNPRSTUVWXY3456789"; let asyncRandomBytes = util.promisify(crypto.randomBytes); @@ -29,7 +27,11 @@ async function generateCode(length) { } -class ControllerPlugin extends lib.BaseControllerPlugin { +type PlayerCode = { playerCode: string, verifyCode: string | null, expires: number }; + +export class ControllerPlugin extends lib.BaseControllerPlugin { + players!: Map; + async init() { // Store of validation attempts by players this.players = new Map(); @@ -44,7 +46,7 @@ class ControllerPlugin extends lib.BaseControllerPlugin { } }, 60e3).unref(); - this.controller.app.get("/api/player_auth/servers", (req, res) => { + this.controller.app.get("/api/player_auth/servers", (req: Request, res: Response) => { let servers = []; for (let instance of this.controller.instances.values()) { if (instance.status === "running" && instance.config.get("player_auth.load_plugin")) { @@ -54,11 +56,11 @@ class ControllerPlugin extends lib.BaseControllerPlugin { res.send(servers); }); - this.controller.app.post("/api/player_auth/player_code", express.json(), (req, res, next) => { + this.controller.app.post("/api/player_auth/player_code", express.json(), (req: Request, res: Response, next: any) => { this.handlePlayerCode(req, res).catch(next); }); - this.controller.app.post("/api/player_auth/verify", express.json(), (req, res, next) => { + this.controller.app.post("/api/player_auth/verify", express.json(), (req: Request, res: Response, next: any) => { this.handleVerify(req, res).catch(next); }); @@ -66,7 +68,7 @@ class ControllerPlugin extends lib.BaseControllerPlugin { this.controller.handle(SetVerifyCodeRequest, this.handleSetVerifyCodeRequest.bind(this)); } - async handlePlayerCode(req, res) { + async handlePlayerCode(req: Request, res: Response) { if (basicType(req.body) !== "object") { res.sendStatus(400); return; @@ -100,7 +102,7 @@ class ControllerPlugin extends lib.BaseControllerPlugin { res.send({ error: true, message: "invalid player_code" }); } - async handleVerify(req, res) { + async handleVerify(req: Request, res: Response) { if (basicType(req.body) !== "object") { res.sendStatus(400); return; @@ -126,7 +128,7 @@ class ControllerPlugin extends lib.BaseControllerPlugin { let secret = Buffer.from(this.controller.config.get("controller.auth_secret"), "base64"); try { - let payload = jwt.verify(verifyToken, secret, { audience: "player_auth.verify_code" }); + let payload = jwt.verify(verifyToken, secret, { audience: "player_auth.verify_code" }) as jwt.JwtPayload; if (payload.verify_code !== verifyCode) { throw new Error("invalid verify_code"); } @@ -135,7 +137,7 @@ class ControllerPlugin extends lib.BaseControllerPlugin { throw new Error("invalid player_code"); } - } catch (err) { + } catch (err: any) { res.send({ error: true, message: err.message }); return; } @@ -163,21 +165,18 @@ class ControllerPlugin extends lib.BaseControllerPlugin { res.send({ error: true, message: "invalid player_code" }); } - async handleFetchPlayerCodeRequest(request) { + async handleFetchPlayerCodeRequest(request: FetchPlayerCodeRequest) { let playerCode = await generateCode(this.controller.config.get("player_auth.code_length")); let expires = Date.now() + this.controller.config.get("player_auth.code_timeout") * 1000; this.players.set(request.player, { playerCode, verifyCode: null, expires }); return { player_code: playerCode, controller_url: this.controller.getControllerUrl() }; } - async handleSetVerifyCodeRequest(request) { + async handleSetVerifyCodeRequest(request: SetVerifyCodeRequest) { let { player, verifyCode } = request; - if (!this.players.has(player)) { - throw new lib.RequestError("invalid player"); - } let entry = this.players.get(player); - if (entry.expires < Date.now()) { + if (!entry || entry.expires < Date.now()) { throw new lib.RequestError("invalid player"); } @@ -185,9 +184,5 @@ class ControllerPlugin extends lib.BaseControllerPlugin { } } -module.exports = { - ControllerPlugin, - - // For testing only - _generateCode: generateCode, -}; +// For testing only +export const _generateCode = generateCode; diff --git a/plugins/player_auth/info.js b/plugins/player_auth/info.ts similarity index 79% rename from plugins/player_auth/info.js rename to plugins/player_auth/info.ts index a04107c1..1d1015c8 100644 --- a/plugins/player_auth/info.js +++ b/plugins/player_auth/info.ts @@ -1,7 +1,5 @@ -"use strict"; -const lib = require("@clusterio/lib"); -let messages = require("./messages"); - +import * as lib from "@clusterio/lib"; +import * as messages from "./messages"; class ControllerConfigGroup extends lib.PluginConfigGroup {} ControllerConfigGroup.defaultAccess = ["controller", "host", "control"]; @@ -22,12 +20,12 @@ ControllerConfigGroup.define({ }); ControllerConfigGroup.finalize(); -module.exports.default = { +const info: lib.PluginInfo = { name: "player_auth", title: "Player Auth", description: "Provides authentication to the cluster via logging into a Factorio server.", - controllerEntrypoint: "controller", - instanceEntrypoint: "instance", + controllerEntrypoint: "dist/plugin/controller", + instanceEntrypoint: "dist/plugin/instance", webEntrypoint: "./web", ControllerConfigGroup, @@ -36,3 +34,5 @@ module.exports.default = { messages.SetVerifyCodeRequest, ], }; + +export default info; diff --git a/plugins/player_auth/instance.js b/plugins/player_auth/instance.ts similarity index 67% rename from plugins/player_auth/instance.js rename to plugins/player_auth/instance.ts index ffb740ce..302ccea6 100644 --- a/plugins/player_auth/instance.js +++ b/plugins/player_auth/instance.ts @@ -1,24 +1,28 @@ -/** - * @module - */ -"use strict"; -const lib = require("@clusterio/lib"); +import * as lib from "@clusterio/lib"; -const { FetchPlayerCodeRequest, SetVerifyCodeRequest } = require("./messages"); +import { FetchPlayerCodeRequest, SetVerifyCodeRequest } from "./messages"; +type ipcPlayerAuth = { + type: "open_dialog", + player: string, +} | { + type: "set_verify_code", + player: string, + verify_code: string, +} -class InstancePlugin extends lib.BaseInstancePlugin { +export class InstancePlugin extends lib.BaseInstancePlugin { async init() { if (!this.instance.config.get("factorio.enable_save_patching")) { throw new Error("player_auth plugin requires save patching."); } - this.instance.server.on("ipc-player_auth", event => this.handleEvent(event).catch( + this.instance.server.on("ipc-player_auth", (ipcPlayerAuth: ipcPlayerAuth) => this.handleEvent(ipcPlayerAuth).catch( err => this.logger.error(`Error handling event:\n${err.stack}`) )); } - async handleEvent(event) { + async handleEvent(event: ipcPlayerAuth) { if (event.type === "open_dialog") { if (!this.host.connector.connected) { await this.sendRcon(`/web-login error ${event.player} login is temporarily unavailable`); @@ -28,7 +32,7 @@ class InstancePlugin extends lib.BaseInstancePlugin { let response; try { response = await this.instance.sendTo("controller", new FetchPlayerCodeRequest(event.player)); - } catch (err) { + } catch (err: any) { await this.sendRcon(`/web-login error ${event.player} ${err.message}`); return; } @@ -38,7 +42,7 @@ class InstancePlugin extends lib.BaseInstancePlugin { try { await this.instance.sendTo("controller", new SetVerifyCodeRequest(event.player, event.verify_code)); - } catch (err) { + } catch (err: any) { await this.sendRcon(`/web-login error ${event.player} ${err.message}`); return; } @@ -47,7 +51,3 @@ class InstancePlugin extends lib.BaseInstancePlugin { } } } - -module.exports = { - InstancePlugin, -}; diff --git a/plugins/player_auth/messages.js b/plugins/player_auth/messages.js deleted file mode 100644 index f00f15fd..00000000 --- a/plugins/player_auth/messages.js +++ /dev/null @@ -1,87 +0,0 @@ -"use strict"; - - -class FetchPlayerCodeRequest { - static type = "request"; - static src = "instance"; - static dst = "controller"; - static plugin = "player_auth"; - - /** @type {string} */ - player; - - constructor(player) { - this.player = player; - } - - static jsonSchema = { - type: "object", - required: ["player"], - properties: { - "player": { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.player); - } -} -FetchPlayerCodeRequest.Response = class Response { - /** @type {string} */ - playerCode; - /** @type {string} */ - controllerUrl; - - constructor(playerCode, controllerUrl) { - this.playerCode = playerCode; - this.controllerUrl = controllerUrl; - } - - static jsonSchema = { - type: "object", - required: ["playerCode", "controllerUrl"], - properties: { - "playerCode": { type: "string" }, - "controllerUrl": { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.playerCode, json.controllerUrl); - } -}; - -class SetVerifyCodeRequest { - static type = "request"; - static src = "instance"; - static dst = "controller"; - static plugin = "player_auth"; - - /** @type {string} */ - player; - /** @type {string} */ - verifyCode; - - constructor(player, verifyCode) { - this.player = player; - this.verifyCode = verifyCode; - } - - static jsonSchema = { - type: "object", - required: ["player", "verifyCode"], - properties: { - "player": { type: "string" }, - "verifyCode": { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.playerCode, json.verifyCode); - } -} - -module.exports = { - FetchPlayerCodeRequest, - SetVerifyCodeRequest, -}; diff --git a/plugins/player_auth/messages.ts b/plugins/player_auth/messages.ts new file mode 100644 index 00000000..e141684d --- /dev/null +++ b/plugins/player_auth/messages.ts @@ -0,0 +1,63 @@ +import { Type, Static } from "@sinclair/typebox"; + +class FetchPlayerCodeResponse { + constructor( + public playerCode: string, + public controllerUrl: string + ) { + } + + static jsonSchema = Type.Object({ + "playerCode": Type.String(), + "controllerUrl": Type.String(), + }) + + static fromJSON(json: Static): FetchPlayerCodeResponse { + return new this(json.playerCode, json.controllerUrl); + } +}; + +export class FetchPlayerCodeRequest { + declare ["constructor"]: typeof FetchPlayerCodeRequest; + static type = "request" as const; + static src = "instance" as const; + static dst = "controller" as const; + static plugin = "player_auth" as const; + static Response = FetchPlayerCodeResponse; + + constructor( + public player: string + ) { + } + + static jsonSchema = Type.Object({ + "player": Type.String(), + }) + + static fromJSON(json: Static): FetchPlayerCodeRequest { + return new this(json.player); + } +} + +export class SetVerifyCodeRequest { + declare ["constructor"]: typeof SetVerifyCodeRequest; + static type = "request" as const; + static src = "instance" as const; + static dst = "controller" as const; + static plugin = "player_auth" as const; + + constructor( + public player: string, + public verifyCode: string + ) { + } + + static jsonSchema = Type.Object({ + "player": Type.String(), + "verifyCode": Type.String(), + }) + + static fromJSON(json: Static): SetVerifyCodeRequest { + return new this(json.player, json.verifyCode); + } +} diff --git a/plugins/player_auth/package.json b/plugins/player_auth/package.json index 2e67e6c2..b5277157 100644 --- a/plugins/player_auth/package.json +++ b/plugins/player_auth/package.json @@ -6,9 +6,9 @@ "homepage": "https://github.com/clusterio/clusterio#readme", "license": "MIT", "repository": "clusterio/clusterio", - "main": "./info.js", + "main": "dist/plugin/info.js", "scripts": { - "prepare": "webpack-cli --env production", + "prepare": "tsc --build && webpack-cli --env production", "test": "echo \"Error: run tests from root\" && exit 1" }, "bugs": { @@ -21,16 +21,21 @@ "@clusterio/lib": "^2.0.0-alpha.0" }, "dependencies": { + "@sinclair/typebox": "^0.30.4", "express": "^4.16.2", "jsonwebtoken": "^8.2.1" }, "devDependencies": { "@clusterio/lib": "^2.0.0-alpha.13", "@clusterio/web_ui": "^2.0.0-alpha.13", + "@types/express": "^4.17.17", + "@types/jsonwebtoken": "^9.0.2", + "@types/node": "^20.4.5", "antd": "^5.7.3", "phin": "^3.7.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "typescript": "^5.1.6", "webpack": "^5.88.2", "webpack-cli": "^5.1.4", "webpack-merge": "^5.9.0" diff --git a/plugins/player_auth/test/plugin.js b/plugins/player_auth/test/plugin.js index 87219c08..5e7e81fb 100644 --- a/plugins/player_auth/test/plugin.js +++ b/plugins/player_auth/test/plugin.js @@ -5,10 +5,10 @@ const jwt = require("jsonwebtoken"); const mock = require("../../../test/mock"); -const controller = require("../controller"); -const instance = require("../instance"); -const info = require("../info").default; -const { FetchPlayerCodeRequest, SetVerifyCodeRequest } = require("../messages"); +const controller = require("../dist/plugin/controller"); +const instance = require("../dist/plugin/instance"); +const info = require("../dist/plugin/info").default; +const { FetchPlayerCodeRequest, SetVerifyCodeRequest } = require("../dist/plugin/messages"); const lib = require("@clusterio/lib"); diff --git a/plugins/player_auth/tsconfig.json b/plugins/player_auth/tsconfig.json new file mode 100644 index 00000000..5408f6b2 --- /dev/null +++ b/plugins/player_auth/tsconfig.json @@ -0,0 +1,16 @@ +{ + "include": ["./**/*.ts"], + "exclude": ["test/*", "./dist/*"], + "compilerOptions": { + "lib": ["es2023"], + "module": "commonjs", + "target": "es2022", + "rootDir": ".", + "outDir": "dist/plugin", + "sourceMap": true, + "strict": true, + "alwaysStrict": true, + "esModuleInterop": true, + "skipLibCheck": true + } +} diff --git a/plugins/player_auth/webpack.config.js b/plugins/player_auth/webpack.config.js index bae8ede2..04c092d2 100644 --- a/plugins/player_auth/webpack.config.js +++ b/plugins/player_auth/webpack.config.js @@ -16,7 +16,7 @@ module.exports = (env = {}) => merge(common(env), { name: "player_auth", library: { type: "var", name: "plugin_player_auth" }, exposes: { - "./info": "./info.js", + "./info": "./dist/plugin/info.js", "./package.json": "./package.json", "./web": "./web/index.jsx", }, diff --git a/plugins/research_sync/controller.js b/plugins/research_sync/controller.ts similarity index 71% rename from plugins/research_sync/controller.js rename to plugins/research_sync/controller.ts index 20503480..cedb268c 100644 --- a/plugins/research_sync/controller.js +++ b/plugins/research_sync/controller.ts @@ -1,42 +1,52 @@ -"use strict"; -const fs = require("fs-extra"); -const path = require("path"); +import fs from "fs-extra"; +import path from "path"; -const lib = require("@clusterio/lib"); +import * as lib from "@clusterio/lib"; const { RateLimiter } = lib; -const { +import { ContributionEvent, ProgressEvent, FinishedEvent, Technology, SyncTechnologiesRequest, -} = require("./messages"); +} from "./messages"; -async function loadTechnologies(controllerConfig, logger) { +async function loadTechnologies( + controllerConfig: lib.ControllerConfig, + logger: lib.Logger +): Promise> { let filePath = path.join(controllerConfig.get("controller.database_directory"), "technologies.json"); logger.verbose(`Loading ${filePath}`); try { - return new Map(JSON.parse(await fs.readFile(filePath))); + return new Map(JSON.parse(await fs.readFile(filePath, { encoding: "utf8" }))); - } catch (err) { + } catch (err: any) { if (err.code === "ENOENT") { logger.verbose("Creating new technologies database"); return new Map(); - } throw err; } } -async function saveTechnologies(controllerConfig, technologies, logger) { +async function saveTechnologies( + controllerConfig: lib.ControllerConfig, + technologies: Map, + logger: lib.Logger +) { let filePath = path.join(controllerConfig.get("controller.database_directory"), "technologies.json"); logger.verbose(`writing ${filePath}`); await lib.safeOutputFile(filePath, JSON.stringify([...technologies.entries()], null, 4)); } -class ControllerPlugin extends lib.BaseControllerPlugin { +export class ControllerPlugin extends lib.BaseControllerPlugin { + technologies!: Map; + progressRateLimiter!: lib.RateLimiter; + progressBroadcastId!: ReturnType | null; + progressToBroadcast!: Set; + async init() { this.technologies = await loadTechnologies(this.controller.config, this.logger); this.progressRateLimiter = new RateLimiter({ @@ -61,8 +71,8 @@ class ControllerPlugin extends lib.BaseControllerPlugin { let techs = []; for (let name of this.progressToBroadcast) { let tech = this.technologies.get(name); - if (tech.progress) { - techs.push({ name, level: tech.level, progress: tech.progress }); + if (tech && tech.progress) { + techs.push(new Technology(name, tech.level, tech.progress, false)); } } this.progressToBroadcast.clear(); @@ -72,11 +82,11 @@ class ControllerPlugin extends lib.BaseControllerPlugin { } } - async handleContributionEvent(event) { + async handleContributionEvent(event: ContributionEvent) { let { name, level, contribution } = event; let tech = this.technologies.get(name); if (!tech) { - tech = { level, progress: 0, researched: false }; + tech = new Technology(name, level, 0, false); this.technologies.set(name, tech); // Ignore contribution to already researched technologies @@ -95,11 +105,11 @@ class ControllerPlugin extends lib.BaseControllerPlugin { return; } - let newProgress = tech.progress + contribution; + let newProgress = tech.progress! + contribution; if (newProgress < 1) { tech.progress = newProgress; this.progressToBroadcast.add(name); - this.progressRateLimiter.activate(); + this.progressRateLimiter!.activate(); } else { tech.researched = true; @@ -110,17 +120,17 @@ class ControllerPlugin extends lib.BaseControllerPlugin { } } - async handleFinishedEvent(event) { + async handleFinishedEvent(event: FinishedEvent) { let { name, level } = event; let tech = this.technologies.get(name); if (!tech || tech.level <= level) { this.progressToBroadcast.delete(name); - this.technologies.set(name, { level, progress: null, researched: true }); + this.technologies.set(name, new Technology(name, level, null, true)); } } - async handleSyncTechnologiesRequest(request) { - function baseLevel(name) { + async handleSyncTechnologiesRequest(request: SyncTechnologiesRequest): Promise { + function baseLevel(name: string): number { let match = /-(\d+)$/.exec(name); if (!match) { return 1; @@ -132,11 +142,11 @@ class ControllerPlugin extends lib.BaseControllerPlugin { let { name, level, progress, researched } = instanceTech; let tech = this.technologies.get(name); if (!tech) { - this.technologies.set(name, { level, progress, researched }); + this.technologies.set(name, new Technology(name, level, progress, researched)); if (progress) { this.progressToBroadcast.add(name); } else if (researched || baseLevel(name) !== level) { - this.controller.sendTo("allInstances", new FinishedEvent(name, tech)); + this.controller.sendTo("allInstances", new FinishedEvent(name, level)); } } else { @@ -146,8 +156,8 @@ class ControllerPlugin extends lib.BaseControllerPlugin { if (tech.level < level || researched) { // Send update if the unlocked level is greater - if (level - !researched > tech.level - !tech.researched) { - this.controller.sendTo("allInstances", new FinishedEvent(name, level - !researched)); + if (level - Number(!researched) > tech.level - Number(!tech.researched)) { + this.controller.sendTo("allInstances", new FinishedEvent(name, level - Number(!researched))); } tech.level = level; tech.progress = progress; @@ -159,7 +169,7 @@ class ControllerPlugin extends lib.BaseControllerPlugin { this.progressToBroadcast.delete(name); } - } else if (tech.progress < progress) { + } else if (tech.progress && progress && tech.progress < progress) { tech.progress = progress; this.progressToBroadcast.add(name); } @@ -175,8 +185,3 @@ class ControllerPlugin extends lib.BaseControllerPlugin { return technologies; } } - - -module.exports = { - ControllerPlugin, -}; diff --git a/plugins/research_sync/info.js b/plugins/research_sync/info.ts similarity index 53% rename from plugins/research_sync/info.js rename to plugins/research_sync/info.ts index b651529b..cffbee27 100644 --- a/plugins/research_sync/info.js +++ b/plugins/research_sync/info.ts @@ -1,12 +1,13 @@ -"use strict"; -const messages = require("./messages"); +import type { PluginInfo } from "@clusterio/lib"; -module.exports.default = { +import * as messages from "./messages"; + +const info: PluginInfo = { name: "research_sync", title: "Research Sync", description: "Synchronises technology research progress between instances.", - instanceEntrypoint: "instance", - controllerEntrypoint: "controller", + instanceEntrypoint: "dist/plugin/instance", + controllerEntrypoint: "dist/plugin/controller", messages: [ messages.ContributionEvent, @@ -15,3 +16,5 @@ module.exports.default = { messages.SyncTechnologiesRequest, ], }; + +export default info; diff --git a/plugins/research_sync/instance.js b/plugins/research_sync/instance.ts similarity index 79% rename from plugins/research_sync/instance.js rename to plugins/research_sync/instance.ts index 9529ff10..b1e7bc01 100644 --- a/plugins/research_sync/instance.js +++ b/plugins/research_sync/instance.ts @@ -1,16 +1,27 @@ -"use strict"; -const lib = require("@clusterio/lib"); -const { +import * as lib from "@clusterio/lib"; +import { ContributionEvent, ProgressEvent, FinishedEvent, Technology, SyncTechnologiesRequest, -} = require("./messages"); +} from "./messages"; +// ./module/sync.lua +type IpcContribution = { + name: string, + level: number, + contribution: number, +}; +type IpcFinished = { + name: string, + level: number, +}; -class InstancePlugin extends lib.BaseInstancePlugin { - unexpectedError(err) { +export class InstancePlugin extends lib.BaseInstancePlugin { + syncStarted!: boolean; + + unexpectedError(err: Error) { this.logger.error(`Unexpected error:\n${err.stack}`); } @@ -19,10 +30,10 @@ class InstancePlugin extends lib.BaseInstancePlugin { throw new Error("research_sync plugin requires save patching."); } - this.instance.server.on("ipc-research_sync:contribution", (tech) => { + this.instance.server.on("ipc-research_sync:contribution", (tech: IpcContribution) => { this.researchContribution(tech).catch(err => this.unexpectedError(err)); }); - this.instance.server.on("ipc-research_sync:finished", (tech) => { + this.instance.server.on("ipc-research_sync:finished", (tech: IpcFinished) => { this.researchFinished(tech).catch(err => this.unexpectedError(err)); }); @@ -31,11 +42,11 @@ class InstancePlugin extends lib.BaseInstancePlugin { this.instance.handle(FinishedEvent, this.handleFinishedEvent.bind(this)); } - async researchContribution(tech) { + async researchContribution(tech: IpcContribution) { this.instance.sendTo("controller", new ContributionEvent(tech.name, tech.level, tech.contribution)); } - async handleProgressEvent(event) { + async handleProgressEvent(event: ProgressEvent) { if (!this.syncStarted || !["starting", "running"].includes(this.instance.status)) { return; } @@ -43,11 +54,11 @@ class InstancePlugin extends lib.BaseInstancePlugin { await this.sendOrderedRcon(`/sc research_sync.update_progress("${techsJson}")`, true); } - async researchFinished(tech) { + async researchFinished(tech: IpcFinished) { this.instance.sendTo("allInstances", new FinishedEvent(tech.name, tech.level)); } - async handleFinishedEvent(event) { + async handleFinishedEvent(event: FinishedEvent) { if (!this.syncStarted || !["starting", "running"].includes(this.instance.status)) { return; } @@ -93,7 +104,3 @@ class InstancePlugin extends lib.BaseInstancePlugin { } } } - -module.exports = { - InstancePlugin, -}; diff --git a/plugins/research_sync/messages.js b/plugins/research_sync/messages.js deleted file mode 100644 index 7e37ec05..00000000 --- a/plugins/research_sync/messages.js +++ /dev/null @@ -1,179 +0,0 @@ -"use strict"; -const lib = require("@clusterio/lib"); - -class ContributionEvent { - static type = "event"; - static src = "instance"; - static dst = "controller"; - static plugin = "research_sync"; - - /** @type {string} */ - name; - /** @type {number} */ - level; - /** @type {number} */ - contribution; - - constructor(name, level, contribution) { - this.name = name; - this.level = level; - this.contribution = contribution; - } - - static jsonSchema = { - type: "object", - required: ["name", "level", "contribution"], - properties: { - name: { type: "string" }, - level: { type: "integer" }, - contribution: { type: "number" }, - }, - }; - - static fromJSON(json) { - return new this(json.name, json.level, json.contribution); - } -} - -class ProgressEvent { - static type = "event"; - static src = "controller"; - static dst = "instance"; - static plugin = "research_sync"; - - /** @type {Array} */ - technologies; - - constructor(technologies) { - this.technologies = technologies; - } - - static jsonSchema = { - type: "object", - required: ["technologies"], - properties: { - technologies: { - type: "array", - items: { - type: "object", - required: ["name", "level", "progress"], - properties: { - name: { type: "string" }, - level: { type: "integer" }, - progress: { type: "number" }, - }, - additionalProperties: false, - }, - }, - }, - }; - - static fromJSON(json) { - return new this(json.technologies); - } -} - -class FinishedEvent { - static type = "event"; - static src = ["instance", "controller"]; - static dst = "instance"; - static plugin = "research_sync"; - - /** @type {string} */ - name; - /** @type {number} */ - level; - - constructor(name, level) { - this.name = name; - this.level = level; - } - - static jsonSchema = { - type: "object", - required: ["name", "level"], - properties: { - name: { type: "string" }, - level: { type: "integer" }, - }, - }; - - static fromJSON(json) { - return new this(json.name, json.level); - } -} - -class Technology { - /** @type {string} */ - name; - /** @type {number} */ - level; - /** @type {?number} */ - progress; - /** @type {boolean} */ - researched; - - constructor(name, level, progress, researched) { - this.name = name; - this.level = level; - this.progress = progress; - this.researched = researched; - } - - static jsonSchema = { - type: "array", - minItems: 4, - maxItems: 4, - items: [ - { type: "string" }, - { type: "integer" }, - { type: ["null", "number"] }, - { type: "boolean" }, - ], - }; - - toJSON() { - return [this.name, this.level, this.progress, this.researched]; - } - - static fromJSON(json) { - return new this(json[0], json[1], json[2], json[3]); - } -} - -class SyncTechnologiesRequest { - static type = "request"; - static src = "instance"; - static dst = "controller"; - static plugin = "research_sync"; - - /** @type {Array} */ - technologies; - - constructor(technologies) { - this.technologies = technologies; - } - - static jsonSchema = { - type: "array", - items: Technology.jsonSchema, - }; - - toJSON() { - return this.technologies; - } - - static fromJSON(json) { - return new this(json.map(e => Technology.fromJSON(e))); - } - - static Response = lib.jsonArray(Technology); -} - -module.exports = { - ContributionEvent, - ProgressEvent, - FinishedEvent, - Technology, - SyncTechnologiesRequest, -}; diff --git a/plugins/research_sync/messages.ts b/plugins/research_sync/messages.ts new file mode 100644 index 00000000..81a33d98 --- /dev/null +++ b/plugins/research_sync/messages.ts @@ -0,0 +1,137 @@ +import { Type, Static } from "@sinclair/typebox"; +import * as lib from "@clusterio/lib"; + +export class ContributionEvent { + declare ["constructor"]: typeof ContributionEvent; + static type = "event" as const; + static src = "instance" as const; + static dst = "controller" as const; + static plugin = "research_sync" as const; + + constructor( + public name: string, + public level: number, + public contribution: number + ) { + } + + static jsonSchema = Type.Object({ + "name": Type.String(), + "level": Type.Integer(), + "contribution": Type.Number(), + }) + + static fromJSON(json: Static): ContributionEvent { + return new this(json.name, json.level, json.contribution); + } +} + + +export class TechnologyProgress { + constructor( + public name: string, + public level: number, + public progress: number | null, + ) { + } + + static jsonSchema = Type.Object({ + "name": Type.String(), + "level": Type.Integer(), + "progress": Type.Number(), + }) +} + +export class ProgressEvent { + declare ["constructor"]: typeof ProgressEvent; + static type = "event" as const; + static src = "controller" as const; + static dst = "instance" as const; + static plugin = "research_sync" as const; + + constructor( + public technologies: TechnologyProgress[], + ) { + } + + static jsonSchema = Type.Object({ + "technologies": Type.Array( + TechnologyProgress.jsonSchema + ) + }) + + static fromJSON(json: Static): ProgressEvent { + return new this(json.technologies); + } +} + +export class FinishedEvent { + declare ["constructor"]: typeof FinishedEvent; + static type = "event" as const; + static src = ["instance", "controller"] as const; + static dst = "instance" as const; + static plugin = "research_sync" as const; + + constructor( + public name: string, + public level: number, + ) { + } + + static jsonSchema = Type.Object({ + "name": Type.String(), + "level": Type.Integer(), + }) + + static fromJSON(json: Static): FinishedEvent { + return new this(json.name, json.level); + } +} + +export class Technology { + constructor( + public name: string, + public level: number, + public progress: number | null, + public researched: boolean, + ) { + } + + static jsonSchema = Type.Tuple([ + Type.String(), + Type.Integer(), + Type.Union([ Type.Number(), Type.Null() ]), + Type.Boolean(), + ]) + toJSON() { + return [this.name, this.level, this.progress, this.researched]; + } + + static fromJSON(json: Static): Technology { + return new this(json[0], json[1], json[2], json[3]); + } +} + +export class SyncTechnologiesRequest { + declare ["constructor"]: typeof SyncTechnologiesRequest; + static type = "request" as const; + static src = "instance" as const; + static dst = "controller" as const; + static plugin = "research_sync" as const; + + constructor( + public technologies: Technology[] + ) { + } + + static jsonSchema = Type.Array(Technology.jsonSchema); + toJSON() { + return this.technologies; + } + + static fromJSON(json: Static): SyncTechnologiesRequest { + return new this(json.map(e => Technology.fromJSON(e))); + } + + static Response = lib.jsonArray(Technology); +} diff --git a/plugins/research_sync/package.json b/plugins/research_sync/package.json index 43379241..9ce41934 100644 --- a/plugins/research_sync/package.json +++ b/plugins/research_sync/package.json @@ -6,9 +6,9 @@ "homepage": "https://github.com/clusterio/clusterio#readme", "license": "MIT", "repository": "clusterio/clusterio", - "main": "./info.js", + "main": "dist/plugin/info.js", "scripts": { - "prepare": "webpack-cli --env production", + "prepare": "tsc --build && webpack-cli --env production", "test": "echo \"Error: run tests from root\" && exit 1" }, "bugs": { @@ -21,11 +21,15 @@ "@clusterio/lib": "^2.0.0-alpha.0" }, "dependencies": { + "@sinclair/typebox": "^0.30.4", "fs-extra": "^8.1.0" }, "devDependencies": { "@clusterio/lib": "^2.0.0-alpha.13", "@clusterio/web_ui": "^2.0.0-alpha.13", + "@types/node": "^20.4.5", + "@types/fs-extra": "^11.0.1", + "typescript": "^5.1.6", "webpack": "^5.88.2", "webpack-cli": "^5.1.4", "webpack-merge": "^5.9.0" diff --git a/plugins/research_sync/test/missing.js b/plugins/research_sync/test/missing.js index 43fe98c8..790baadb 100644 --- a/plugins/research_sync/test/missing.js +++ b/plugins/research_sync/test/missing.js @@ -2,5 +2,5 @@ // required here in order to track coverage for them. "use strict"; -require("../instance"); -require("../controller"); +require("../dist/plugin/instance"); +require("../dist/plugin/controller"); diff --git a/plugins/research_sync/tsconfig.json b/plugins/research_sync/tsconfig.json new file mode 100644 index 00000000..5408f6b2 --- /dev/null +++ b/plugins/research_sync/tsconfig.json @@ -0,0 +1,16 @@ +{ + "include": ["./**/*.ts"], + "exclude": ["test/*", "./dist/*"], + "compilerOptions": { + "lib": ["es2023"], + "module": "commonjs", + "target": "es2022", + "rootDir": ".", + "outDir": "dist/plugin", + "sourceMap": true, + "strict": true, + "alwaysStrict": true, + "esModuleInterop": true, + "skipLibCheck": true + } +} diff --git a/plugins/research_sync/webpack.config.js b/plugins/research_sync/webpack.config.js index 0f2f0fba..069b6dac 100644 --- a/plugins/research_sync/webpack.config.js +++ b/plugins/research_sync/webpack.config.js @@ -16,7 +16,7 @@ module.exports = (env = {}) => merge(common(env), { name: "research_sync", library: { type: "var", name: "plugin_research_sync" }, exposes: { - "./info": "./info.js", + "./info": "./dist/plugin/info.js", "./package.json": "./package.json", }, shared: { diff --git a/plugins/statistics_exporter/info.js b/plugins/statistics_exporter/info.ts similarity index 83% rename from plugins/statistics_exporter/info.js rename to plugins/statistics_exporter/info.ts index 60a2f6ab..6ca7b680 100644 --- a/plugins/statistics_exporter/info.js +++ b/plugins/statistics_exporter/info.ts @@ -1,5 +1,4 @@ -"use strict"; -const lib = require("@clusterio/lib"); +import * as lib from "@clusterio/lib"; class InstanceConfigGroup extends lib.PluginConfigGroup {} InstanceConfigGroup.defaultAccess = ["controller", "host", "control"]; @@ -13,12 +12,14 @@ InstanceConfigGroup.define({ }); InstanceConfigGroup.finalize(); -module.exports.default = { +const info: lib.PluginInfo = { name: "statistics_exporter", title: "Statistics Exporter", description: "Provides in-game item/fluid production, builds, kills, and pollution "+ "statistics to the cluster's Prometheus endpoint.", - instanceEntrypoint: "instance", + instanceEntrypoint: "dist/plugin/instance", InstanceConfigGroup, }; + +export default info; diff --git a/plugins/statistics_exporter/instance.js b/plugins/statistics_exporter/instance.ts similarity index 77% rename from plugins/statistics_exporter/instance.js rename to plugins/statistics_exporter/instance.ts index 5c9d699f..f67f3f6e 100644 --- a/plugins/statistics_exporter/instance.js +++ b/plugins/statistics_exporter/instance.ts @@ -1,7 +1,23 @@ -"use strict"; -const lib = require("@clusterio/lib"); +import * as lib from "@clusterio/lib"; const { Gauge } = lib; +type IpcStats = { + game_tick: number, + player_count: number, + game_flow_statistics: { + pollution_statistics: IpcFlowType, + }, + force_flow_statistics: { + [key:string]: { + [key:string]: IpcFlowType, + } + }, +} + +type IpcFlowType = { + "input": Record, + "output": Record, +} const instancePlayerCount = new Gauge( "clusterio_statistics_exporter_instance_player_count", @@ -24,7 +40,14 @@ const instanceGameFlowStatistics = new Gauge( { labels: ["instance_id", "statistic", "direction", "name"] }, ); -function setForceFlowStatistic(instanceId, forceName, statisticName, direction, item, value) { +function setForceFlowStatistic( + instanceId: number, + forceName: string, + statisticName: string, + direction: string, + item: string, + value: number, +) { instanceForceFlowStatistics.labels(String(instanceId), forceName, statisticName, direction, item).set(value); // For item and fluid statistics it's useful to compare the input flow with the @@ -36,7 +59,7 @@ function setForceFlowStatistic(instanceId, forceName, statisticName, direction, } -class InstancePlugin extends lib.BaseInstancePlugin { +export class InstancePlugin extends lib.BaseInstancePlugin { async init() { if (!this.instance.config.get("factorio.enable_save_patching")) { throw new Error("statistics_exporter plugin requires save patching."); @@ -45,10 +68,10 @@ class InstancePlugin extends lib.BaseInstancePlugin { async gatherMetrics() { let string = await this.sendRcon("/sc statistics_exporter.export()"); - let stats; + let stats: IpcStats; try { stats = JSON.parse(string); - } catch (err) { + } catch (err: any) { throw new Error(`Error parsing statistics JSON: ${err.message}, content "${string}"`); } @@ -85,16 +108,11 @@ class InstancePlugin extends lib.BaseInstancePlugin { // stream is overloaded. Should the timeout be exceeded the // previous values for the metrics will end up being sent to controller. let timeout = this.instance.config.get("statistics_exporter.command_timeout") * 1000; - await lib.timeout(this.gatherMetrics(), timeout); + await lib.timeout(this.gatherMetrics(), timeout, undefined); } } -module.exports = { - InstancePlugin, - - // For testing only - _instancePlayerCount: instancePlayerCount, - _instanceGameTicksTotal: instanceGameTicksTotal, - _instanceForceFlowStatistics: instanceForceFlowStatistics, - _instanceGameFlowStatistics: instanceGameFlowStatistics, -}; +export const _instancePlayerCount = instancePlayerCount; +export const _instanceGameTicksTotal = instanceGameTicksTotal; +export const _instanceForceFlowStatistics = instanceForceFlowStatistics; +export const _instanceGameFlowStatistics = instanceGameFlowStatistics; diff --git a/plugins/statistics_exporter/package.json b/plugins/statistics_exporter/package.json index c30ffe1b..7bce6e46 100644 --- a/plugins/statistics_exporter/package.json +++ b/plugins/statistics_exporter/package.json @@ -6,9 +6,9 @@ "homepage": "https://github.com/clusterio/clusterio#readme", "license": "MIT", "repository": "clusterio/clusterio", - "main": "./info.js", + "main": "dist/plugin/info.js", "scripts": { - "prepare": "webpack-cli --env production", + "prepare": "tsc --build && webpack-cli --env production", "test": "echo \"Error: run tests from root\" && exit 1" }, "bugs": { diff --git a/plugins/statistics_exporter/test/plugin.js b/plugins/statistics_exporter/test/plugin.js index 9d9a616c..43356fcb 100644 --- a/plugins/statistics_exporter/test/plugin.js +++ b/plugins/statistics_exporter/test/plugin.js @@ -2,8 +2,8 @@ const assert = require("assert").strict; const mock = require("../../../test/mock"); -const instance = require("../instance"); -const info = require("../info").default; +const instance = require("../dist/plugin/instance"); +const info = require("../dist/plugin/info").default; describe("statistics_exporter plugin", function() { diff --git a/plugins/statistics_exporter/tsconfig.json b/plugins/statistics_exporter/tsconfig.json new file mode 100644 index 00000000..5408f6b2 --- /dev/null +++ b/plugins/statistics_exporter/tsconfig.json @@ -0,0 +1,16 @@ +{ + "include": ["./**/*.ts"], + "exclude": ["test/*", "./dist/*"], + "compilerOptions": { + "lib": ["es2023"], + "module": "commonjs", + "target": "es2022", + "rootDir": ".", + "outDir": "dist/plugin", + "sourceMap": true, + "strict": true, + "alwaysStrict": true, + "esModuleInterop": true, + "skipLibCheck": true + } +} diff --git a/plugins/statistics_exporter/webpack.config.js b/plugins/statistics_exporter/webpack.config.js index d62c818f..515da3ef 100644 --- a/plugins/statistics_exporter/webpack.config.js +++ b/plugins/statistics_exporter/webpack.config.js @@ -16,7 +16,7 @@ module.exports = (env = {}) => merge(common(env), { name: "statistics_exporter", library: { type: "var", name: "plugin_statistics_exporter" }, exposes: { - "./info": "./info.js", + "./info": "./dist/plugin/info.js", "./package.json": "./package.json", }, shared: { diff --git a/plugins/subspace_storage/controller.js b/plugins/subspace_storage/controller.ts similarity index 74% rename from plugins/subspace_storage/controller.js rename to plugins/subspace_storage/controller.ts index 823b7566..30cbc20e 100644 --- a/plugins/subspace_storage/controller.js +++ b/plugins/subspace_storage/controller.ts @@ -1,21 +1,22 @@ -"use strict"; -const fs = require("fs-extra"); -const path = require("path"); +import type ControlConnection from "@clusterio/controller/src/ControlConnection"; -const lib = require("@clusterio/lib"); -const { Counter, Gauge, RateLimiter } = lib; +import fs from "fs-extra"; +import path from "path"; -const routes = require("./routes"); -const dole = require("./dole"); +import * as lib from "@clusterio/lib"; +const { Counter, Gauge } = lib; -const { +import * as routes from "./routes"; +import * as dole from "./dole"; + +import { Item, PlaceEvent, RemoveRequest, GetStorageRequest, UpdateStorageEvent, SetStorageSubscriptionRequest, -} = require("./messages"); +} from "./messages"; const exportCounter = new Counter( @@ -35,14 +36,17 @@ const controllerInventoryGauge = new Gauge( ); -async function loadDatabase(config, logger) { +async function loadDatabase( + config: lib.ControllerConfig, + logger: lib.Logger +): Promise { let itemsPath = path.resolve(config.get("controller.database_directory"), "items.json"); logger.verbose(`Loading ${itemsPath}`); try { - let content = await fs.readFile(itemsPath); + let content = await fs.readFile(itemsPath, { encoding: "utf8" }); return new lib.ItemDatabase(JSON.parse(content)); - } catch (err) { + } catch (err: any) { if (err.code === "ENOENT") { logger.verbose("Creating new item database"); return new lib.ItemDatabase(); @@ -51,7 +55,11 @@ async function loadDatabase(config, logger) { } } -async function saveDatabase(controllerConfig, items, logger) { +async function saveDatabase( + controllerConfig: lib.ControllerConfig, + items: lib.ItemDatabase, + logger: lib.Logger, +) { if (items && items.size < 50000) { let file = path.resolve(controllerConfig.get("controller.database_directory"), "items.json"); logger.verbose(`writing ${file}`); @@ -62,21 +70,28 @@ async function saveDatabase(controllerConfig, items, logger) { } } -class ControllerPlugin extends lib.BaseControllerPlugin { - async init() { +export class ControllerPlugin extends lib.BaseControllerPlugin { + items!: lib.ItemDatabase; + itemUpdateRateLimiter!: lib.RateLimiter; + itemsLastUpdate!: Map; + subscribedControlLinks!: Set; + autosaveId!: ReturnType; + doleMagicId!: ReturnType; + neuralDole!: dole.NeuralDole; + async init() { this.items = await loadDatabase(this.controller.config, this.logger); - this.itemUpdateRateLimiter = new RateLimiter({ + this.itemUpdateRateLimiter = new lib.RateLimiter({ maxRate: 1, action: () => { try { this.broadcastStorage(); - } catch (err) { + } catch (err: any) { this.logger.error(`Unexpected error sending storage update:\n${err.stack}`); } }, }); - this.itemsLastUpdate = new Map(this.items._items.entries()); + this.itemsLastUpdate = new Map(this.items.getEntries()); this.autosaveId = setInterval(() => { saveDatabase(this.controller.config, this.items, this.logger).catch(err => { this.logger.error(`Unexpected error autosaving items:\n${err.stack}`); @@ -105,32 +120,32 @@ class ControllerPlugin extends lib.BaseControllerPlugin { } broadcastStorage() { - let itemsToUpdate = new Map(); - for (let [name, count] of this.items._items) { + let itemsToUpdateMap:Map = new Map(); + for (let [name, count] of this.items.getEntries()) { if (this.itemsLastUpdate.get(name) === count) { continue; } - itemsToUpdate.set(name, count); + itemsToUpdateMap.set(name, count); } - if (!itemsToUpdate.size) { + if (!itemsToUpdateMap.size) { return; } - itemsToUpdate = [...itemsToUpdate.entries()]; - let update = new UpdateStorageEvent(itemsToUpdate); + let itemsToUpdate = [...itemsToUpdateMap.entries()]; + let update = UpdateStorageEvent.fromJSON({ items: itemsToUpdate }); this.controller.sendTo("allInstances", update); for (let link of this.subscribedControlLinks) { link.send(update); } - this.itemsLastUpdate = new Map(this.items._items.entries()); + this.itemsLastUpdate = new Map(this.items.getEntries()); } async handleGetStorageRequest() { - return [...this.items._items.entries()]; + return [...this.items.getEntries()]; } - async handlePlaceEvent(request, src) { + async handlePlaceEvent(request: PlaceEvent, src: lib.Address) { let instanceId = src.id; for (let item of request.items) { @@ -147,7 +162,7 @@ class ControllerPlugin extends lib.BaseControllerPlugin { } } - async handleRemoveRequest(request, src) { + async handleRemoveRequest(request: RemoveRequest, src: lib.Address) { let method = this.controller.config.get("subspace_storage.division_method"); let instanceId = src.id; @@ -171,7 +186,7 @@ class ControllerPlugin extends lib.BaseControllerPlugin { // use fancy neural net to calculate a "fair" dole division rate. if (method === "neural_dole") { for (let item of request.items) { - let count = neuralDole.divider({ name: item.name, count: item.count, instanceId, instanceName }); + let count = this.neuralDole.divider({ name: item.name, count: item.count, instanceId, instanceName }); if (count > 0) { itemsRemoved.push(new Item(item.name, count)); } @@ -212,7 +227,7 @@ class ControllerPlugin extends lib.BaseControllerPlugin { return itemsRemoved; } - async handleSetStorageSubscriptionRequest(request, src) { + async handleSetStorageSubscriptionRequest(request: SetStorageSubscriptionRequest, src: lib.Address) { let link = this.controller.wsServer.controlConnections.get(src.id); if (request.storage) { this.subscribedControlLinks.add(link); @@ -221,15 +236,15 @@ class ControllerPlugin extends lib.BaseControllerPlugin { } } - onControlConnectionEvent(connection, event) { + onControlConnectionEvent(connection: ControlConnection, event: string) { if (event === "close") { this.subscribedControlLinks.delete(connection); } } - onMetrics() { + async onMetrics() { if (this.items) { - for (let [key, count] of this.items._items) { + for (let [key, count] of this.items.getEntries()) { controllerInventoryGauge.labels(key).set(Number(count) || 0); } } @@ -242,7 +257,3 @@ class ControllerPlugin extends lib.BaseControllerPlugin { await saveDatabase(this.controller.config, this.items, this.logger); } } - -module.exports = { - ControllerPlugin, -}; diff --git a/plugins/subspace_storage/dole.js b/plugins/subspace_storage/dole.ts similarity index 73% rename from plugins/subspace_storage/dole.js rename to plugins/subspace_storage/dole.ts index 142530f3..35579b9b 100644 --- a/plugins/subspace_storage/dole.js +++ b/plugins/subspace_storage/dole.ts @@ -1,7 +1,6 @@ -"use strict"; -const doleNN = require("./dole_nn_base"); +import * as doleNN from "./dole_nn_base"; -const lib = require("@clusterio/lib"); +import * as lib from "@clusterio/lib"; const { Gauge } = lib; @@ -17,8 +16,8 @@ const prometheusDoleFactorGauge = new Gauge( { labels: ["resource"] } ); -class NeuralDole { - getRequestStats(itemname, samples) { +export class NeuralDole { + getRequestStats(itemname: string, samples: number): number { let sum = 0; samples = Math.min(samples, (this.stats[itemname] || []).length - 1); if (samples < 1) { @@ -33,25 +32,25 @@ class NeuralDole { return sum / samples; } - constructor({ - items, - }) { + items: lib.ItemDatabase; + itemsLastTick: Map; + dole: any = {}; + carry: any = {}; + lastRequest: any = {}; + stats: any = {}; + debt: any = {}; + constructor({ items }: { items: lib.ItemDatabase }) { // Set some storage variables for the dole divider this.items = items; - this.itemsLastTick = new Map(items._items); - this.dole = {}; - this.carry = {}; - this.lastRequest = {}; - this.stats=[]; - this.debt= {}; + this.itemsLastTick = new Map(items.getEntries()); } doMagic() { - for (let [name, count] of this.items._items) { + for (let [name, count] of this.items.getEntries()) { let magicData = doleNN.tick( count, this.dole[name], - this.itemsLastTick.get(name), + this.itemsLastTick.get(name) || 0, this.getRequestStats(name, 5) ); this.stats[name] = this.stats[name] || []; @@ -64,31 +63,31 @@ class NeuralDole { // DONE handle magicData[1] for graphing for our users prometheusNNDoleGauge.labels(name).set(magicData[1] || 0); } - this.itemsLastTick = new Map(this.items._items); + this.itemsLastTick = new Map(this.items.getEntries()); } - divider(object) { + divider(object: { name:string, count:number, instanceId:number, instanceName:string }): number { let magicData = doleNN.dose( object.count, // numReq - this.items.getItemCount(object.name), + this.items.getItemCount(object.name) || 0, this.itemsLastTick.get(object.name) || 0, this.dole[object.name], - this.carry[`${object.name} ${object.instanceID}`] || 0, - this.lastRequest[`${object.name}_${object.instanceID}_${object.instanceName}`] || 0, + this.carry[`${object.name} ${object.instanceId}`] || 0, + this.lastRequest[`${object.name}_${object.instanceId}_${object.instanceName}`] || 0, this.getRequestStats(object.name, 5), - this.debt[`${object.name} ${object.instanceID}`] || 0 + this.debt[`${object.name} ${object.instanceId}`] || 0 ); if ((this.stats[object.name] || []).length > 0) { this.stats[object.name][0].req += Number(object.count); this.stats[object.name][0].given += Number(magicData[0]); } - this.lastRequest[`${object.name}_${object.instanceID}_${object.instanceName}`] = object.count; + this.lastRequest[`${object.name}_${object.instanceId}_${object.instanceName}`] = object.count; // 0. Number of items to give in that dose // 1. New dole for item X // 2. New carry for item X instance Y this.dole[object.name] = magicData[1]; - this.carry[`${object.name} ${object.instanceID}`] = magicData[2]; - this.debt[`${object.name} ${object.instanceID}`] = magicData[3]; + this.carry[`${object.name} ${object.instanceId}`] = magicData[2]; + this.debt[`${object.name} ${object.instanceId}`] = magicData[3]; // Remove item from DB and send it this.items.removeItem(object.name, magicData[0]); @@ -99,13 +98,19 @@ class NeuralDole { // If the server regularly can't fulfill requests, this number grows until // it can. Then it slowly shrinks back down. -let _doleDivisionFactor = {}; -function doleDivider({ - object, - items, - logItemTransfers, - logger, -}) { +let _doleDivisionFactor: { [key:string]: number } = {}; +export function doleDivider({ + object, + items, + logItemTransfers, + logger, + } :{ + object: { name:string, count:number, instanceId:number, instanceName:string }, + items: lib.ItemDatabase, + logItemTransfers: boolean, + logger: lib.Logger, + }, +) { let itemCount = items.getItemCount(object.name); // lower rates will equal more dramatic swings const doleDivisionRetardation = 10; @@ -143,8 +148,3 @@ function doleDivider({ return 0; // console.log(`failure out of ${object.name}/${object.count} from ${object.instanceID} (${object.instanceName})`); } - -module.exports = { - doleDivider, - NeuralDole, -}; diff --git a/plugins/subspace_storage/dole_nn_base.js b/plugins/subspace_storage/dole_nn_base.ts similarity index 88% rename from plugins/subspace_storage/dole_nn_base.js rename to plugins/subspace_storage/dole_nn_base.ts index 1818a76b..6db51d02 100644 --- a/plugins/subspace_storage/dole_nn_base.js +++ b/plugins/subspace_storage/dole_nn_base.ts @@ -1,25 +1,24 @@ // Sigmoids... -"use strict"; -function sigmoid0(x) { +function sigmoid0(x: number): number { return x / (1 + Math.abs(x)); } -function sigmoid1(x) { +function sigmoid1(x: number): number { if (x >= 0.0) { return 1.0; } return 0.0; } -function sigmoid2(x) { +function sigmoid2(x: number): number { if (x >= 0.0) { return x; } return 0.0; } -function sigmoid3(x) { +function sigmoid3(x: number) { return x; } @@ -36,12 +35,21 @@ function sigmoid3(x) { // requests for item, numinstance - number of instances requesting that item // debt - debt for item X instance Y -// Returns: Array of 3 elements +// Returns: Array of 4 elements // 0.Number of items to give in that dose // 1.New dole for item X // 2.New carry for item X instance Y // 3.New debt -function dose(numreq, instore, store_last_tick, dole, carry, prev_req, numreq_total_adj, debt) { +export function dose( + numreq: number, + instore: number, + store_last_tick: number, + dole: number, + carry: number, + prev_req: number, + numreq_total_adj: number, + debt: number, +): [number, number, number, number] { numreq = Number(numreq); instore = Number(instore); let instore_adj = instore; @@ -115,7 +123,12 @@ function dose(numreq, instore, store_last_tick, dole, carry, prev_req, numreq_to // Returns: Array of 2 elements // 0.New dole for item X // 1.Guessed percentage average of items supplied in last 10 ticks -function tick(instore, dole, store_last_tick, numreq_total_adj) { +export function tick( + instore: number, + dole: number, + store_last_tick: number, + numreq_total_adj: number, +): [number, number] { let instore_adj = instore; if (instore_adj === 0) { instore_adj = 0.1; @@ -148,5 +161,3 @@ function tick(instore, dole, store_last_tick, numreq_total_adj) { return [sigmoid0(outputs[0]), avg]; } - -module.exports = { dose, tick }; diff --git a/plugins/subspace_storage/info.js b/plugins/subspace_storage/info.ts similarity index 91% rename from plugins/subspace_storage/info.js rename to plugins/subspace_storage/info.ts index 92876000..58d7a60d 100644 --- a/plugins/subspace_storage/info.js +++ b/plugins/subspace_storage/info.ts @@ -1,6 +1,5 @@ -"use strict"; -const lib = require("@clusterio/lib"); -const messages = require("./messages"); +import * as lib from "@clusterio/lib"; +import * as messages from "./messages"; class ControllerConfigGroup extends lib.PluginConfigGroup {} ControllerConfigGroup.defaultAccess = ["controller", "host", "control"]; @@ -52,10 +51,10 @@ module.exports.default = { name: "subspace_storage", title: "Subspace Storage", description: "Provides shared storage across instances for the Subspace Storage mod", - instanceEntrypoint: "instance", + instanceEntrypoint: "dist/plugin/instance", InstanceConfigGroup, - controllerEntrypoint: "controller", + controllerEntrypoint: "dist/plugin/controller", ControllerConfigGroup, messages: [ diff --git a/plugins/subspace_storage/instance.js b/plugins/subspace_storage/instance.ts similarity index 79% rename from plugins/subspace_storage/instance.js rename to plugins/subspace_storage/instance.ts index 41b826db..c6b83156 100644 --- a/plugins/subspace_storage/instance.js +++ b/plugins/subspace_storage/instance.ts @@ -1,24 +1,29 @@ -"use strict"; -const lib = require("@clusterio/lib"); +import * as lib from "@clusterio/lib"; -const { +import { PlaceEvent, RemoveRequest, GetStorageRequest, UpdateStorageEvent, -} = require("./messages"); + Item, +} from "./messages"; -class InstancePlugin extends lib.BaseInstancePlugin { - unexpectedError(err) { +type IpcItems = { name: string, count: number }[]; + +export class InstancePlugin extends lib.BaseInstancePlugin { + pendingTasks!: Set; + pingId?: ReturnType; + + unexpectedError(err: Error) { this.logger.error(`Unexpected error:\n${err.stack}`); } async init() { this.pendingTasks = new Set(); - this.instance.server.on("ipc-subspace_storage:output", (output) => { + this.instance.server.on("ipc-subspace_storage:output", (output: IpcItems) => { this.provideItems(output).catch(err => this.unexpectedError(err)); }); - this.instance.server.on("ipc-subspace_storage:orders", (orders) => { + this.instance.server.on("ipc-subspace_storage:orders", (orders: IpcItems) => { if (this.instance.status !== "running" || !this.host.connected) { return; } @@ -57,7 +62,7 @@ class InstancePlugin extends lib.BaseInstancePlugin { } // provide items -------------------------------------------------------------- - async provideItems(items) { + async provideItems(items: IpcItems) { if (!this.host.connector.hasSession) { // For now the items are voided if the controller connection is // down, which is no different from the previous behaviour. @@ -68,7 +73,8 @@ class InstancePlugin extends lib.BaseInstancePlugin { return; } - this.instance.sendTo("controller", new PlaceEvent(items)); + const fromIpcItems = items.map(item => new Item(item.name, item.count)) + this.instance.sendTo("controller", new PlaceEvent(fromIpcItems)); if (this.instance.config.get("subspace_storage.log_item_transfers")) { this.logger.verbose("Exported the following to controller:"); @@ -77,9 +83,10 @@ class InstancePlugin extends lib.BaseInstancePlugin { } // request items -------------------------------------------------------------- - async requestItems(requestItems) { + async requestItems(requestItems: IpcItems) { // Request the items all at once - let items = await this.instance.sendTo("controller", new RemoveRequest(requestItems)); + let fromIpcItems = requestItems.map(item => new Item(item.name, item.count)) + let items = await this.instance.sendTo("controller", new RemoveRequest(fromIpcItems)); if (!items.length) { return; @@ -95,14 +102,14 @@ class InstancePlugin extends lib.BaseInstancePlugin { } // combinator signals --------------------------------------------------------- - async handleUpdateStorageEvent(request) { + async handleUpdateStorageEvent(event: UpdateStorageEvent) { if (this.instance.status !== "running") { return; } - let items = request.items; + let items = event.items; // XXX this should be moved to instance/clusterio api - items.push(["signal-unixtime", Math.floor(Date.now()/1000)]); + items.push(new Item("signal-unixtime", Math.floor(Date.now()/1000))); let itemsJson = lib.escapeString(JSON.stringify(items)); let task = this.sendRcon(`/sc __subspace_storage__ UpdateInvData("${itemsJson}")`, true); @@ -111,7 +118,3 @@ class InstancePlugin extends lib.BaseInstancePlugin { await task; } } - -module.exports = { - InstancePlugin, -}; diff --git a/plugins/subspace_storage/messages.js b/plugins/subspace_storage/messages.js deleted file mode 100644 index b27332eb..00000000 --- a/plugins/subspace_storage/messages.js +++ /dev/null @@ -1,167 +0,0 @@ -"use strict"; -const lib = require("@clusterio/lib"); - -class Item { - /** @type {string} */ - name; - /** @type {number} */ - count; - - constructor(name, count) { - this.name = name; - this.count = count; - } - - static jsonSchema = { - type: "array", - minItems: 2, - maxItems: 2, - items: [ - { type: "string" }, - { type: "integer" }, - ], - }; - - toJSON() { - return [this.name, this.count]; - } - - static fromJSON(json) { - return new this(json[0], json[1]); - } -} - -// XXX this should be a request to be reliable -class PlaceEvent { - static type = "event"; - static src = "instance"; - static dst = "controller"; - static plugin = "subspace_storage"; - - /** @type {Array} */ - items; - - constructor(items) { - this.items = items; - } - - static jsonSchema = { - type: "object", - required: ["items"], - properties: { - "items": { - type: "array", - items: Item.jsonSchema, - }, - }, - }; - - static fromJSON(json) { - return new this(json.items); - } -} - -class RemoveRequest { - static type = "request"; - static src = "instance"; - static dst = "controller"; - static plugin = "subspace_storage"; - - /** @type {Array} */ - items; - - constructor(items) { - this.items = items; - } - - static jsonSchema = { - type: "object", - required: ["items"], - properties: { - "items": { - type: "array", - items: Item.jsonSchema, - }, - }, - }; - - static fromJSON(json) { - return new this(json.items); - } - - static Response = lib.jsonArray(Item); -} - -class GetStorageRequest { - static type = "request"; - static src = ["instance", "control"]; - static dst = "controller"; - static plugin = "subspace_storage"; - static permission = "subspace_storage.storage.view"; - static Response = lib.jsonArray(Item); -} - -class UpdateStorageEvent { - static type = "request"; - static src = "controller"; - static dst = ["instance", "control"]; - static plugin = "subspace_storage"; - - /** @type {Array} */ - items; - - constructor(items) { - this.items = items; - } - - static jsonSchema = { - type: "object", - required: ["items"], - properties: { - "items": { - type: "array", - items: Item.jsonSchema, - }, - }, - }; - - static fromJSON(json) { - return new this(json.items); - } -} - -class SetStorageSubscriptionRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static plugin = "subspace_storage"; - static permission = "subspace_storage.storage.view"; - - /** @type {boolean} */ - storage; - - constructor(storage) { - this.storage = storage; - } - - static jsonSchema = { - type: "object", - required: ["storage"], - properties: { - "storage": { type: "boolean" }, - }, - }; - - static fromJSON(json) { - return new this(json.storage); - } -} - -module.exports = { - Item, - PlaceEvent, - RemoveRequest, - GetStorageRequest, - UpdateStorageEvent, - SetStorageSubscriptionRequest, -}; diff --git a/plugins/subspace_storage/messages.ts b/plugins/subspace_storage/messages.ts new file mode 100644 index 00000000..24ebedc2 --- /dev/null +++ b/plugins/subspace_storage/messages.ts @@ -0,0 +1,122 @@ +import { Type, Static } from "@sinclair/typebox"; +import * as lib from "@clusterio/lib"; +import { type } from "os"; + +export class Item { + constructor( + public name: string, + public count: number + ) { + } + + static jsonSchema = Type.Tuple([ + Type.String(), + Type.Number(), + ]); + + toJSON() { + return [this.name, this.count]; + } + + static fromJSON(json: Static): Item { + return new this(json[0], json[1]); + } +} + +// XXX this should be a request to be reliable +export class PlaceEvent { + declare ["constructor"]: typeof PlaceEvent; + static type = "event" as const; + static src = "instance" as const; + static dst = "controller" as const; + static plugin = "subspace_storage" as const; + + constructor( + public items: Item[] + ) { + } + + static jsonSchema = Type.Object({ + "items": Type.Array(Item.jsonSchema) + }) + + static fromJSON(json: Static): PlaceEvent { + return new this(json.items.map(item => Item.fromJSON(item))); + } +} + +export class RemoveRequest { + declare ["constructor"]: typeof RemoveRequest; + static type = "request" as const; + static src = "instance" as const; + static dst = "controller" as const; + static plugin = "subspace_storage" as const; + + constructor( + public items: Item[] + ) { + } + + static jsonSchema = Type.Object({ + "items": Type.Array(Item.jsonSchema) + }) + + static fromJSON(json: Static): RemoveRequest { + return new this(json.items.map(item => Item.fromJSON(item))); + } + + static Response = lib.jsonArray(Item); +} + +export class GetStorageRequest { + declare ["constructor"]: typeof GetStorageRequest; + static type = "request" as const; + static src = ["instance", "control"] as const; + static dst = "controller" as const; + static plugin = "subspace_storage" as const; + static permission = "subspace_storage.storage.view" as const; + static Response = lib.jsonArray(Item); +} + +export class UpdateStorageEvent { + declare ["constructor"]: typeof UpdateStorageEvent; + static type = "request" as const; + static src = "controller" as const; + static dst = ["instance", "control"] as const; + static plugin = "subspace_storage" as const; + + constructor( + public items: Item[] + ) { + } + + static jsonSchema = Type.Object({ + "items": Type.Array(Item.jsonSchema) + }) + + static fromJSON(json: Static): UpdateStorageEvent { + return new this(json.items.map(item => Item.fromJSON(item))); + } +} + +export class SetStorageSubscriptionRequest { + declare ["constructor"]: typeof SetStorageSubscriptionRequest; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static plugin = "subspace_storage" as const; + static permission = "subspace_storage.storage.view" as const; + + constructor( + public storage: boolean + ) { + } + + static jsonSchema = Type.Object({ + "storage": Type.Boolean() + }) + + static fromJSON(json: Static): SetStorageSubscriptionRequest { + return new this(json.storage); + } +} diff --git a/plugins/subspace_storage/package.json b/plugins/subspace_storage/package.json index c4ac748f..4812d7a8 100644 --- a/plugins/subspace_storage/package.json +++ b/plugins/subspace_storage/package.json @@ -6,9 +6,9 @@ "homepage": "https://github.com/clusterio/clusterio#readme", "license": "MIT", "repository": "clusterio/clusterio", - "main": "./info.js", + "main": "dist/plugin/info.js", "scripts": { - "prepare": "webpack-cli --env production", + "prepare": "tsc --build && webpack-cli --env production", "test": "echo \"Error: run tests from root\" && exit 1" }, "bugs": { @@ -21,11 +21,15 @@ "@clusterio/lib": "^2.0.0-alpha.0" }, "dependencies": { + "@sinclair/typebox": "^0.30.4", "fs-extra": "^8.1.0" }, "devDependencies": { "@clusterio/lib": "^2.0.0-alpha.13", "@clusterio/web_ui": "^2.0.0-alpha.13", + "@types/express": "^4.17.17", + "@types/fs-extra": "^11.0.1", + "@types/node": "^20.4.5", "antd": "^5.7.3", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/plugins/subspace_storage/routes.js b/plugins/subspace_storage/routes.ts similarity index 71% rename from plugins/subspace_storage/routes.js rename to plugins/subspace_storage/routes.ts index e24239db..b97b4326 100644 --- a/plugins/subspace_storage/routes.js +++ b/plugins/subspace_storage/routes.ts @@ -1,5 +1,7 @@ -"use strict"; -function addApiRoutes(app, items) { +import type { Application, Request, Response } from "express"; +import * as lib from "@clusterio/lib"; + +export function addApiRoutes(app: Application, items: lib.ItemDatabase) { /** * GET endpoint to read the controllers current inventory of items. @@ -9,12 +11,12 @@ function addApiRoutes(app, items) { * @alias api/inventory * @returns {object[]} JSON [{name:"iron-plate", count:100},{name:"copper-plate",count:5}] */ - app.get("/api/inventory", (req, res) => { + app.get("/api/inventory", (req: Request, res: Response) => { res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); // Check it and send it let inventory = []; - for (let [name, count] of items._items) { + for (let [name, count] of items.getEntries()) { inventory.push({ name, count }); } res.type("json"); @@ -29,15 +31,11 @@ function addApiRoutes(app, items) { * @alias api/inventoryAsObject * @returns {object} JSON {"iron-plate":100, "copper-plate":5} */ - app.get("/api/inventoryAsObject", (req, res) => { + app.get("/api/inventoryAsObject", (req: Request, res: Response) => { res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); // Check it and send it res.type("json"); - res.send(JSON.stringify(items.serialise())); + res.send(JSON.stringify(items.serialize())); }); } - -module.exports = { - addApiRoutes, -}; diff --git a/plugins/subspace_storage/test/missing.js b/plugins/subspace_storage/test/missing.js index caa7f4a1..6f7e6a1b 100644 --- a/plugins/subspace_storage/test/missing.js +++ b/plugins/subspace_storage/test/missing.js @@ -2,7 +2,7 @@ // required here in order to track coverage for them. "use strict"; -require("../instance"); -require("../controller"); -require("../dole"); -require("../dole_nn_base"); +require("../dist/plugin/instance"); +require("../dist/plugin/controller"); +require("../dist/plugin/dole"); +require("../dist/plugin/dole_nn_base"); diff --git a/plugins/subspace_storage/test/routes.js b/plugins/subspace_storage/test/routes.js index d5110b29..00de92b0 100644 --- a/plugins/subspace_storage/test/routes.js +++ b/plugins/subspace_storage/test/routes.js @@ -1,7 +1,7 @@ "use strict"; const assert = require("assert").strict; -const routes = require("../routes"); +const routes = require("../dist/plugin/routes"); const { get } = require("../../../test/integration"); diff --git a/plugins/subspace_storage/tsconfig.json b/plugins/subspace_storage/tsconfig.json new file mode 100644 index 00000000..5408f6b2 --- /dev/null +++ b/plugins/subspace_storage/tsconfig.json @@ -0,0 +1,16 @@ +{ + "include": ["./**/*.ts"], + "exclude": ["test/*", "./dist/*"], + "compilerOptions": { + "lib": ["es2023"], + "module": "commonjs", + "target": "es2022", + "rootDir": ".", + "outDir": "dist/plugin", + "sourceMap": true, + "strict": true, + "alwaysStrict": true, + "esModuleInterop": true, + "skipLibCheck": true + } +} diff --git a/plugins/subspace_storage/web/index.jsx b/plugins/subspace_storage/web/index.jsx index 4121d995..d5b211ec 100644 --- a/plugins/subspace_storage/web/index.jsx +++ b/plugins/subspace_storage/web/index.jsx @@ -3,7 +3,7 @@ import { Input, Table, Typography } from "antd"; import * as lib from "@clusterio/lib"; import { notifyErrorHandler, useItemMetadata, useLocale, PageLayout, ControlContext } from "@clusterio/web_ui"; -import { GetStorageRequest, SetStorageSubscriptionRequest } from "../messages"; +import { GetStorageRequest, SetStorageSubscriptionRequest } from "../dist/plugin/messages"; import "./index.css"; diff --git a/plugins/subspace_storage/webpack.config.js b/plugins/subspace_storage/webpack.config.js index 23a0b41f..87a02824 100644 --- a/plugins/subspace_storage/webpack.config.js +++ b/plugins/subspace_storage/webpack.config.js @@ -16,7 +16,7 @@ module.exports = (env = {}) => merge(common(env), { name: "subspace_storage", library: { type: "var", name: "plugin_subspace_storage" }, exposes: { - "./info": "./info.js", + "./info": "./dist/plugin/info.js", "./package.json": "./package.json", "./web": "./web/index.jsx", }, diff --git a/test/integration/index.js b/test/integration/index.js index 2ceb2356..e9274609 100644 --- a/test/integration/index.js +++ b/test/integration/index.js @@ -13,10 +13,10 @@ const { LineSplitter, ConsoleTransport, logger } = lib; // Make sure permissions from plugins are loaded require("../../plugins/global_chat/dist/plugin/info"); -require("../../plugins/player_auth/info"); -require("../../plugins/research_sync/info"); -require("../../plugins/statistics_exporter/info"); -require("../../plugins/subspace_storage/info"); +require("../../plugins/player_auth/dist/plugin/info"); +require("../../plugins/research_sync/dist/plugin/info"); +require("../../plugins/statistics_exporter/dist/plugin/info"); +require("../../plugins/subspace_storage/dist/plugin/info"); class TestControl extends lib.Link {