mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-14 15:24:52 +09:00
Port remaining plugins to Typescript (#499)
* .js to .ts * Allow access to _items entries; getItemCount return number * add permission field to PluginPage interface * port plugins to typescript * update Request classes * fix IpcFlowType
This commit is contained in:
1 parent
50f12c719b
commit
0b3550b472
51 files changed
+1033
-1026
No files matched your search
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<string, IpcPlayerData>,
|
||||
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<string, { instanceId: number, expires?: number }>;
|
||||
playerDatastore!: Map<string, IpcPlayerData>;
|
||||
autosaveId!: ReturnType<typeof setInterval>;
|
||||
|
||||
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,
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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<string>;
|
||||
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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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<typeof AcquireResponse.jsonSchema>): 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<typeof AcquireRequest.jsonSchema>): 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<typeof ReleaseRequest.jsonSchema>) {
|
||||
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<typeof UploadRequest.jsonSchema>) {
|
||||
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<typeof DownloadResponse.jsonSchema>): 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<typeof DownloadRequest.jsonSchema>) {
|
||||
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<typeof DatabaseStatsResponse.jsonSchema>): 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;
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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<string> {
|
||||
// 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<string, PlayerCode>;
|
||||
|
||||
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;
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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<typeof FetchPlayerCodeResponse.jsonSchema>): 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<typeof FetchPlayerCodeRequest.jsonSchema>): 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<typeof SetVerifyCodeRequest.jsonSchema>): SetVerifyCodeRequest {
|
||||
return new this(json.player, json.verifyCode);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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<Map<any, any>> {
|
||||
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<any, any>,
|
||||
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<string, Technology>;
|
||||
progressRateLimiter!: lib.RateLimiter;
|
||||
progressBroadcastId!: ReturnType<typeof setInterval> | null;
|
||||
progressToBroadcast!: Set<string>;
|
||||
|
||||
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<Technology[]> {
|
||||
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,
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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<Technology>} */
|
||||
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<Technology>} */
|
||||
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,
|
||||
};
|
||||
@@ -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<typeof ContributionEvent.jsonSchema>): 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<typeof ProgressEvent.jsonSchema>): 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<typeof FinishedEvent.jsonSchema>): 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<typeof Technology.jsonSchema>): 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<typeof SyncTechnologiesRequest.jsonSchema>): SyncTechnologiesRequest {
|
||||
return new this(json.map(e => Technology.fromJSON(e)));
|
||||
}
|
||||
|
||||
static Response = lib.jsonArray(Technology);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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");
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
@@ -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<string, number>,
|
||||
"output": Record<string, number>,
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -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": {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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<lib.ItemDatabase> {
|
||||
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<string, number>;
|
||||
subscribedControlLinks!: Set<ControlConnection>;
|
||||
autosaveId!: ReturnType<typeof setInterval>;
|
||||
doleMagicId!: ReturnType<typeof setInterval>;
|
||||
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<string, number> = 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,
|
||||
};
|
||||
@@ -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<string, number>;
|
||||
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,
|
||||
};
|
||||
+21
-10
@@ -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 };
|
||||
@@ -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: [
|
||||
@@ -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<any>;
|
||||
pingId?: ReturnType<typeof setTimeout>;
|
||||
|
||||
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,
|
||||
};
|
||||
@@ -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<Item>} */
|
||||
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<Item>} */
|
||||
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<Item>} */
|
||||
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,
|
||||
};
|
||||
@@ -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<typeof Item.jsonSchema>): 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<typeof PlaceEvent.jsonSchema>): 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<typeof RemoveRequest.jsonSchema>): 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<typeof UpdateStorageEvent.jsonSchema>): 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<typeof SetStorageSubscriptionRequest.jsonSchema>): SetStorageSubscriptionRequest {
|
||||
return new this(json.storage);
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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");
|
||||
@@ -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");
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user