UNPKG

@clusterio/plugin-subspace_storage

Version:

Clusterio plugin for sharing storage between Factorio servers

256 lines 11.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ControllerPlugin = void 0; const controller_1 = require("@clusterio/controller"); const fs_extra_1 = __importDefault(require("fs-extra")); const path_1 = __importDefault(require("path")); const lib = __importStar(require("@clusterio/lib")); const { Counter, Gauge } = lib; const routes = __importStar(require("./routes")); const dole = __importStar(require("./dole")); const messages_1 = require("./messages"); const exportCounter = new Counter("clusterio_subspace_storage_export_total", "Resources exported by instance", { labels: ["instance_id", "resource", "quality"] }); const importCounter = new Counter("clusterio_subspace_storage_import_total", "Resources imported by instance", { labels: ["instance_id", "resource", "quality"] }); const controllerInventoryGauge = new Gauge("clusterio_subspace_storage_controller_inventory", "Amount of resources stored on controller", { labels: ["resource", "quality"] }); async function loadDatabase(config, logger) { let itemsPath = path_1.default.resolve(config.get("controller.database_directory"), "items.json"); logger.verbose(`Loading ${itemsPath}`); try { let content = await fs_extra_1.default.readFile(itemsPath, { encoding: "utf8" }); return new lib.ItemDatabase(JSON.parse(content)); } catch (err) { if (err.code === "ENOENT") { logger.verbose("Creating new item database"); return new lib.ItemDatabase(); } throw err; } } async function saveDatabase(controllerConfig, items, logger) { if (items && items.size < 50000) { let file = path_1.default.resolve(controllerConfig.get("controller.database_directory"), "items.json"); logger.verbose(`writing ${file}`); let content = JSON.stringify(items.serialize()); await lib.safeOutputFile(file, content); } else if (items) { logger.error(`Item database too large, not saving (${items.size})`); } } class ControllerPlugin extends controller_1.BaseControllerPlugin { items; itemUpdateRateLimiter; itemsLastUpdate = new Map(); subscribedControlLinks; doleMagicId; neuralDole; storageDirty = false; async init() { this.items = await loadDatabase(this.controller.config, this.logger); this.itemUpdateRateLimiter = new lib.RateLimiter({ maxRate: 1, action: () => { try { this.broadcastStorage(); } catch (err) { this.logger.error(`Unexpected error sending storage update:\n${err.stack}`); } }, }); this.itemsLastUpdate = new Map(); for (const [name, qualities] of this.items.getEntries()) { this.itemsLastUpdate.set(name, { ...qualities }); } this.neuralDole = new dole.NeuralDole({ items: this.items }); this.doleMagicId = setInterval(() => { if (this.controller.config.get("subspace_storage.division_method") === "neural_dole") { this.neuralDole.doMagic(); } }, 1000); this.subscribedControlLinks = new Set(); routes.addApiRoutes(this.controller.app, this.items); this.controller.handle(messages_1.GetStorageRequest, this.handleGetStorageRequest.bind(this)); this.controller.handle(messages_1.PlaceEvent, this.handlePlaceEvent.bind(this)); this.controller.handle(messages_1.RemoveRequest, this.handleRemoveRequest.bind(this)); this.controller.handle(messages_1.SetStorageSubscriptionRequest, this.handleSetStorageSubscriptionRequest.bind(this)); } updateStorage() { this.itemUpdateRateLimiter.activate(); this.storageDirty = true; } broadcastStorage() { let itemsToUpdate = []; for (const [name, qualities] of this.items.getEntries()) { const lastQualities = this.itemsLastUpdate.get(name); for (const [quality, count] of Object.entries(qualities)) { if (!lastQualities || lastQualities[quality] !== count) { itemsToUpdate.push(new messages_1.Item(name, count, quality)); } } } if (!itemsToUpdate.length) { return; } let update = new messages_1.UpdateStorageEvent(itemsToUpdate); this.controller.sendTo("allInstances", update); for (let link of this.subscribedControlLinks) { link.send(update); } this.itemsLastUpdate = new Map(); for (const [name, qualities] of this.items.getEntries()) { this.itemsLastUpdate.set(name, { ...qualities }); } } async handleGetStorageRequest() { const result = []; for (const [name, qualities] of this.items.getEntries()) { for (const [quality, count] of Object.entries(qualities)) { result.push(new messages_1.Item(name, count, quality)); } } return result; } async handlePlaceEvent(request, src) { let instanceId = src.id; for (let item of request.items) { this.items.addItem(item.name, item.count, item.quality || "normal"); exportCounter.labels(String(instanceId), item.name, item.quality || "normal").inc(item.count); } this.updateStorage(); if (this.controller.config.get("subspace_storage.log_item_transfers")) { this.logger.verbose(`Imported the following from ${instanceId}:\n${JSON.stringify(request.items)}`); } } async handleRemoveRequest(request, src) { let method = this.controller.config.get("subspace_storage.division_method"); let instanceId = src.id; let itemsRemoved = []; if (method === "simple") { for (let item of request.items) { const quality = item.quality || "normal"; let count = this.items.getItemCount(item.name, quality); let toRemove = Math.min(count, item.count); if (toRemove > 0) { this.items.removeItem(item.name, toRemove, quality); itemsRemoved.push(new messages_1.Item(item.name, toRemove, quality)); } } } else { let instance = this.controller.instances.get(instanceId); let instanceName = instance ? instance.config.get("instance.name") : "unknown"; // use fancy neural net to calculate a "fair" dole division rate. if (method === "neural_dole") { for (let item of request.items) { const quality = item.quality || "normal"; let count = this.neuralDole.divider({ name: item.name, quality, count: item.count, instanceId, instanceName }); if (count > 0) { itemsRemoved.push(new messages_1.Item(item.name, count, quality)); } } // Use dole division. Makes it really slow to drain out the last little bit. } else if (method === "dole") { for (let item of request.items) { const quality = item.quality || "normal"; let count = dole.doleDivider({ object: { name: item.name, quality, count: item.count, instanceId, instanceName }, items: this.items, logItemTransfers: this.controller.config.get("subspace_storage.log_item_transfers"), logger: this.logger, }); if (count > 0) { itemsRemoved.push(new messages_1.Item(item.name, count, quality)); } } // Should not be possible } else { throw Error(`Unknown division_method ${method}`); } } if (itemsRemoved.length) { for (let item of itemsRemoved) { importCounter.labels(String(instanceId), item.name, item.quality || "normal").inc(item.count); } this.updateStorage(); if (itemsRemoved.length && this.controller.config.get("subspace_storage.log_item_transfers")) { this.logger.verbose(`Exported the following to ${instanceId}:\n${JSON.stringify(itemsRemoved)}`); } } return itemsRemoved; } async handleSetStorageSubscriptionRequest(request, src) { let link = this.controller.wsServer.controlConnections.get(src.id); if (request.storage) { this.subscribedControlLinks.add(link); } else { this.subscribedControlLinks.delete(link); } } onControlConnectionEvent(connection, event) { if (event === "close") { this.subscribedControlLinks.delete(connection); } } async onMetrics() { if (this.items) { for (const [name, qualities] of this.items.getEntries()) { for (const [quality, count] of Object.entries(qualities)) { controllerInventoryGauge.labels(name, quality).set(Number(count) || 0); } } } } async onShutdown() { this.itemUpdateRateLimiter.cancel(); clearInterval(this.doleMagicId); } async onSaveData() { if (this.storageDirty) { this.storageDirty = false; await saveDatabase(this.controller.config, this.items, this.logger); } } } exports.ControllerPlugin = ControllerPlugin; //# sourceMappingURL=controller.js.map