Compare commits

...
16 Commits
Author SHA1 Message Date
Danielv123 7e39b587df 2.1 map exchange strings 2026-01-27 17:00:21 +01:00
Danielv123 e28789c1c0 Fix crash in prometheus logging 2026-01-27 15:57:35 +01:00
Daniel 4caf06aafc Merge pull request #819 from Cooldude2606/feature/login-screen-name
Display cluster name on login screen
2026-01-22 15:45:00 +01:00
Cooldude2606 6b0dd4e9cf Add tests for new api route 2026-01-21 23:47:47 +00:00
Cooldude2606 a2c38ad4e8 Add cluster name to login form 2026-01-21 23:42:53 +00:00
Double 3a823bee01 Update link to message passing protocol specification (#813) 2026-01-01 19:23:54 +00:00
Cooldude2606 98650b9bfb 2.0.0-alpha.22b (plugins)
Due to the bump of lib version, all plugins must also be bumped to 22b
2025-11-30 22:07:37 +00:00
Daniel a781af5b6a Merge pull request #811 from clusterio/copilot/fix-add-mods-button-visibility
Fix button overflow on narrow screens across web interface
2025-11-22 23:21:55 +01:00
copilot-swe-agent[bot]andDanielv123 f202a0975a Add wrap to more Space components and fix table scroll on narrow screens
Co-authored-by: Danielv123 <6792749+Danielv123@users.noreply.github.com>
2025-11-20 10:55:42 +00:00
copilot-swe-agent[bot]andDanielv123 ea4280d184 Add wrap prop to Space components in SectionHeader for mobile responsiveness
Co-authored-by: Danielv123 <6792749+Danielv123@users.noreply.github.com>
2025-11-20 07:52:22 +00:00
copilot-swe-agent[bot] c519ee1389 Initial plan 2025-11-20 07:45:31 +00:00
CopilotandDanielv123 c61b9dbe60 Remove debug log statement from serialize.lua (#808)
* Initial plan

* Remove debug log statement from serialize.lua

Co-authored-by: Danielv123 <6792749+Danielv123@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Danielv123 <6792749+Danielv123@users.noreply.github.com>
2025-11-11 12:21:07 +01:00
Cooldude2606 77f4aa6296 2.0.0-alpha.22b 2025-11-07 21:55:35 +00:00
Cooldude2606 86a9084d56 Fix package updater 2025-11-07 21:48:09 +00:00
Cooldude2606 43def44136 Fix package path for restart required 2025-11-07 21:47:48 +00:00
Cooldude2606 8c1be7210e Skip git checks on npm publish 2025-11-07 20:08:22 +00:00
33 changed files with 135 additions and 40 deletions

No files matched your search

+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
node-version: 22.x
registry-url: https://registry.npmjs.org
- run: pnpm install --no-frozen-lockfile
- run: pnpm -r --filter='./{plugins,packages}/*' publish
- run: pnpm -r --filter='./{plugins,packages}/*' publish --no-git-checks
env:
NODE_AUTH_TOKEN: $${{ secrets.NPM_TOKEN }}
- uses: softprops/action-gh-release@v2
+1
View File
@@ -64,6 +64,7 @@ Many thanks to the following for contributing to this release:
- Fixed "rendered more hooks" error when refreshing hosts page. [#769](https://github.com/clusterio/clusterio/issues/769)
- Fixed inventory combinator time signal not always updating [#610](https://github.com/clusterio/clusterio/issues/610)
- Renamed templates for `.gitignore` and `.npmignore` that caused `create --plugin-template` to fail. [#800](https://github.com/clusterio/clusterio/pull/800)
- Fixed npm update button failing to apply update and restart.
### Breaking changes
+1 -1
View File
@@ -120,7 +120,7 @@ There's also the `add_libraries` function exported by event_handler, which accep
## Communicating with Clusterio
Clusterio uses a homebrew protocol based on sending JSON payloads over a WebSocket connection.
See [socket.md](socket.md) for the implementation details of it.
See [protocol.md](devs/protocol.md) for the implementation details of it.
It's also possible to write a plugin for Clusterio that exposes a custom interface over HTTP, WebSocket or any other technology supported by Node.js.
+1 -1
View File
@@ -3,7 +3,7 @@
"description": "Implementation of Clusterio controller",
"author": "Hornwitser",
"license": "MIT",
"version": "2.0.0-alpha.22",
"version": "2.0.0-alpha.22b",
"main": "dist/node/index.js",
"scripts": {
"prepare": "tsc --build && webpack-cli --mode production",
+1 -1
View File
@@ -1188,7 +1188,7 @@ export default class ControlConnection extends BaseConnection {
if (!this._controller.config.get("controller.allow_remote_updates")) {
throw new lib.RequestError("Remote updates are disabled on this machine");
}
return lib.updatePackage("@clusterio/controller");
return await lib.updatePackage("@clusterio/controller");
}
async handlePluginUpdateRequest(request: lib.PluginUpdateRequest) {
+4 -2
View File
@@ -570,7 +570,8 @@ export default class Controller {
try {
// First check the clusterio version
const runningVersion = this.config.get("controller.version");
const packageJson = await fs.readJSON(path.join(__dirname, "..", "package.json"));
const packageJsonPath = require.resolve("@clusterio/controller/package.json");
const packageJson = await fs.readJSON(packageJsonPath);
if (runningVersion !== packageJson.version) {
this.config.restartRequired = true;
return true;
@@ -586,7 +587,8 @@ export default class Controller {
}
packageName = pluginInfo.npmPackage ?? pluginInfo.name;
const pluginPackageJson = await fs.readJSON(path.join(pluginInfo.requirePath, "package.json"));
const pluginPackageJsonPath = require.resolve(path.posix.join(pluginInfo.requirePath, "package.json"));
const pluginPackageJson = await fs.readJSON(pluginPackageJsonPath);
if (pluginInfo.version !== pluginPackageJson.version) {
this.config.restartRequired = true;
return true;
+7
View File
@@ -134,6 +134,12 @@ function getPlugins(req: Request, res: Response) {
res.send(plugins);
}
function getClusterName(req: Request, res: Response) {
const controller: Controller = req.app.locals.controller;
const clusterName = controller.config.get("controller.name");
res.json({ name: clusterName });
}
function validateHostToken(req: Request, res: Response, next: any) {
let token = req.header("x-access-token");
if (!token) {
@@ -662,6 +668,7 @@ async function uploadMod(req: Request, res: Response) {
export function addRouteHandlers(app: Application) {
app.get("/metrics", (req:Request, res:Response, next:any) => getMetrics(req, res, next).catch(next));
app.get("/api/plugins", getPlugins);
app.get("/api/cluster-name", getClusterName);
app.put("/api/upload-export",
validateHostToken,
(req:Request, res:Response, next:any) => uploadExport(req, res).catch(next)
+1 -1
View File
@@ -3,7 +3,7 @@
"description": "Installer for clusterio",
"author": "Hornwitser",
"license": "MIT",
"version": "2.0.0-alpha.22",
"version": "2.0.0-alpha.22b",
"main": "create.js",
"bin": {
"clusteriocreate": "./create.js"
@@ -70,7 +70,6 @@ function serialize.serialize_equipment_grid(grid)
if equipment.to_be_removed then
entry.t = equipment.to_be_removed
end
log(entry)
table.insert(serialized, entry)
end
end
+1 -1
View File
@@ -3,7 +3,7 @@
"description": "Implementation of Clusterio host server",
"author": "Hornwitser",
"license": "MIT",
"version": "2.0.0-alpha.22",
"version": "2.0.0-alpha.22b",
"main": "dist/node/index.js",
"scripts": {
"prepare": "tsc --build",
+5 -3
View File
@@ -819,7 +819,8 @@ export default class Host extends lib.Link {
try {
// First check the clusterio version
const runningVersion = this.config.get("host.version");
const packageJson = await fs.readJSON(path.join(__dirname, "..", "package.json"));
const packageJsonPath = require.resolve("@clusterio/host/package.json");
const packageJson = await fs.readJSON(packageJsonPath);
if (runningVersion !== packageJson.version) {
this.config.restartRequired = true;
return true;
@@ -835,7 +836,8 @@ export default class Host extends lib.Link {
}
packageName = pluginInfo.npmPackage ?? pluginInfo.name;
const pluginPackageJson = await fs.readJSON(path.join(pluginInfo.requirePath, "package.json"));
const pluginPackageJsonPath = require.resolve(path.posix.join(pluginInfo.requirePath, "package.json"));
const pluginPackageJson = await fs.readJSON(pluginPackageJsonPath);
if (pluginInfo.version !== pluginPackageJson.version) {
this.config.restartRequired = true;
return true;
@@ -1094,7 +1096,7 @@ export default class Host extends lib.Link {
if (!this.config.get("host.allow_remote_updates")) {
throw new lib.RequestError("Remote updates are disabled on this machine");
}
return lib.updatePackage("@clusterio/host");
return await lib.updatePackage("@clusterio/host");
}
async handlePluginUpdateRequest(request: lib.PluginUpdateRequest) {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@clusterio/lib",
"description": "Shared library for Clusterio",
"version": "2.0.0-alpha.22",
"version": "2.0.0-alpha.22b",
"repository": "https://github.com/clusterio/clusterio",
"license": "MIT",
"scripts": {
+54 -4
View File
@@ -1,10 +1,13 @@
import zlib from "zlib";
type FactorioVersion = [number, number, number, number];
class MapReaderState {
pos = 0;
last_position = { x: 0, y: 0 };
/** True when a version greater than 2.0.0 is detected */
v2 = false;
version: FactorioVersion | null = null;
constructor(
public buf: Buffer
) { }
@@ -110,7 +113,7 @@ function readDict<K, V>(
return mapping;
}
function readVersion(state: MapReaderState) {
function readVersion(state: MapReaderState): FactorioVersion {
let major = readUInt16(state);
let minor = readUInt16(state);
let patch = readUInt16(state);
@@ -118,6 +121,24 @@ function readVersion(state: MapReaderState) {
return [major, minor, patch, developer];
}
function isVersionAtLeast(state: MapReaderState, major: number, minor: number, patch: number, developer: number) {
if (!state.version) {
return false;
}
const [vMajor, vMinor, vPatch, vDeveloper] = state.version;
if (vMajor !== major) {
return vMajor > major;
}
if (vMinor !== minor) {
return vMinor > minor;
}
if (vPatch !== patch) {
return vPatch > patch;
}
return vDeveloper >= developer;
}
function readFrequencySizeRichness(state: MapReaderState) {
return {
frequency: readFloat(state),
@@ -262,7 +283,8 @@ function readEnemyEvolution(state: MapReaderState) {
}
function readEnemyExpansion(state: MapReaderState) {
return {
let evolutionGroupSizeFactor: number | null | undefined;
const settings = {
enabled: readOptional(state, readBool),
max_expansion_distance: readOptional(state, readUInt32),
friendly_base_influence_radius: readOptional(state, readUInt32),
@@ -274,9 +296,36 @@ function readEnemyExpansion(state: MapReaderState) {
max_colliding_tiles_coefficient: readOptional(state, readDouble),
settler_group_min_size: readOptional(state, readUInt32),
settler_group_max_size: readOptional(state, readUInt32),
min_expansion_cooldown: readOptional(state, readUInt32),
max_expansion_cooldown: readOptional(state, readUInt32),
min_expansion_cooldown: null,
max_expansion_cooldown: null,
} as {
enabled: boolean | null;
max_expansion_distance: number | null;
friendly_base_influence_radius: number | null;
enemy_building_influence_radius: number | null;
building_coefficient: number | null;
other_base_coefficient: number | null;
neighbouring_chunk_coefficient: number | null;
neighbouring_base_chunk_coefficient: number | null;
max_colliding_tiles_coefficient: number | null;
settler_group_min_size: number | null;
settler_group_max_size: number | null;
min_expansion_cooldown: number | null;
max_expansion_cooldown: number | null;
evolution_group_size_factor?: number;
};
if (isVersionAtLeast(state, 2, 1, 0, 0)) {
evolutionGroupSizeFactor = readOptional(state, readDouble);
}
settings.min_expansion_cooldown = readOptional(state, readUInt32);
settings.max_expansion_cooldown = readOptional(state, readUInt32);
if (evolutionGroupSizeFactor !== undefined) {
settings.evolution_group_size_factor = evolutionGroupSizeFactor ?? 4.0;
}
return settings;
}
function readUnitGroup(state: MapReaderState) {
@@ -427,6 +476,7 @@ export function readMapExchangeString(exchangeString: string) {
try {
const version = readVersion(state);
state.v2 = version[0] >= 2;
state.version = version;
data = {
version: version,
unknown: readUInt8(state),
+3
View File
@@ -275,6 +275,9 @@ function keyToLabels(key: string) {
if (key !== "") {
for (let pair of key.split(",")) {
let [name, value] = pair.split("=", 2);
if (!name || value === undefined) {
continue;
}
labels.set(name, value
.slice(1, -1)
.replace(/\\"/g, "\"")
+1 -1
View File
@@ -6,7 +6,7 @@ import { RequestError } from "./errors";
import { PluginNodeEnvInfo } from "./plugin";
const execAsync = util.promisify(exec);
async function isDev() {
function isDev() {
// dev: <devRoot>/packages/lib/dist/src/rce_ops.js
// prod: <prodRoot>/node_modules/@clusterio/lib/dist/src/rce_ops.js
return __dirname.split(path.sep).at(-5) === "packages"; // opposed to "@clusterio"
+1 -1
View File
@@ -3,7 +3,7 @@
"description": "Clusterio web interface implementation",
"author": "Hornwitser",
"license": "MIT",
"version": "2.0.0-alpha.22",
"version": "2.0.0-alpha.22b",
"browser": "src/index.ts",
"main": "dist/browser/src/index.js",
"scripts": {
@@ -106,7 +106,7 @@ export default function HostViewPage() {
</PageLayout>;
}
let hostButtons = <Space>
let hostButtons = <Space wrap>
<HostControlButton hostId={hostId} canRestart={system?.canRestart} restartRequired={system?.restartRequired}/>
{
account.hasPermission("core.host.revoke_token")
@@ -141,7 +141,7 @@ function InstanceButtons(props: { instance: lib.InstanceDetails }) {
}
},
};
return <Space>
return <Space wrap>
{
account.hasAnyPermission(...InstanceControlButtonPermissions)
&& <InstanceControlButton instance={instance} />
+20 -3
View File
@@ -1,7 +1,8 @@
import React, { Fragment, useContext } from "react";
import React, { Fragment, useContext, useEffect, useState } from "react";
import { Button, Card, Divider, Form, Image, Input, Space, Typography } from "antd";
import LockOutlined from "@ant-design/icons/LockOutlined";
import notify from "../util/notify";
import logo from "../images/logo.png";
import ControlContext from "./ControlContext";
@@ -36,6 +37,23 @@ function TokenAuth(props: SetTokenProps) {
export default function LoginForm(props: SetTokenProps) {
let plugins = useContext(ControlContext).plugins;
const [clusterName, setClusterName] = useState<string>("Clusterio");
useEffect(() => {
(async () => {
try {
let response = await fetch(`${webRoot}api/cluster-name`);
if (response.ok) {
let data = await response.json();
if (data.name) {
setClusterName(data.name);
}
}
} catch (err) {
notify("Failed to load cluster name");
}
})();
}, []);
let loginForms = [];
for (let plugin of plugins.values()) {
@@ -50,7 +68,7 @@ export default function LoginForm(props: SetTokenProps) {
return <Card style={{ maxWidth: "30em" }}>
<Space style={{ width: "100%" }} direction="vertical" align="center">
<h1>Clusterio</h1>
<h1>{clusterName}</h1>
<Image width={128} height={123} src={logo} />
Log in to the cluster using one of the methods below.
</Space>
@@ -60,4 +78,3 @@ export default function LoginForm(props: SetTokenProps) {
</Fragment>)}
</Card>;
}
@@ -664,7 +664,7 @@ function ModsTable(props: ModsTableProps) {
return <>
<SectionHeader
title="Mods"
extra={<Space>
extra={<Space wrap>
<Button
icon={<ToolOutlined />}
disabled={!mods.some(m => m.enabled && m.warning && m.warning !== "wrong_factorio_version")}
+2 -2
View File
@@ -561,7 +561,7 @@ export default function ModsPage() {
<PageHeader title="Mods" />
<SectionHeader
title="Mod Packs"
extra={<Space>
extra={<Space wrap>
{account.hasPermission("core.mod_pack.create") && <ImportModPackButton />}
{account.hasPermission("core.mod_pack.create") && <CreateModPackButton />}
</Space>}
@@ -594,7 +594,7 @@ export default function ModsPage() {
},
})}
/>
<SectionHeader title="Stored Mods" extra={<Space>
<SectionHeader title="Stored Mods" extra={<Space wrap>
<SearchModsButton />
{uploadButton}
</Space>} />
+1 -1
View File
@@ -187,7 +187,7 @@ export function NpmButton(props: { target: lib.AddressShorthand, canRestart?: bo
{
formAction !== undefined
&& account.hasPermission(`core.${props.target === "controller" ? "controller" : "host"}.restart`)
&& <Form.Item label="Restart on completion" name="restart">
&& <Form.Item label="Restart on completion" name="restart" valuePropName="checked">
<Checkbox disabled={props.canRestart === false}/>
</Form.Item>
}
@@ -1,6 +1,6 @@
import React, { useEffect, useContext, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { Button, Checkbox, Form, Input, Popconfirm, Spin } from "antd";
import { Button, Checkbox, Form, Input, Popconfirm, Space, Spin } from "antd";
import DeleteOutlined from "@ant-design/icons/DeleteOutlined";
import * as lib from "@clusterio/lib";
@@ -71,7 +71,7 @@ export default function RoleViewPage() {
>
<PageHeader
title={role.name}
extra={<>
extra={<Space wrap>
{canUpdate && <Button type={edited ? "primary" : "default"} htmlType="submit">Apply</Button>}
{account.hasPermission("core.role.delete") && <Popconfirm
title="Delete role?"
@@ -90,7 +90,7 @@ export default function RoleViewPage() {
<DeleteOutlined />
</Button>
</Popconfirm>}
</>}
</Space>}
/>
<Form.Item name="name" label="Name">
<Input disabled={!canUpdate}/>
+1 -1
View File
@@ -345,7 +345,7 @@ export default function SavesList(props: { instance: lib.InstanceDetails }) {
};
return <div>
<SectionHeader title="Saves" extra={<Space>
<SectionHeader title="Saves" extra={<Space wrap>
{account.hasPermission("core.instance.save.upload") && <Upload {...uploadProps} >
<UploadButton disabled={hostOffline}>
Upload save
@@ -106,7 +106,7 @@ export default function UserViewPage() {
{user.isBanned && <Tag color="red">Banned</Tag>}
</span>
</Space>}
extra={<>
extra={<Space wrap>
{account.hasPermission("core.user.revoke_token") && (
account.name === userName || account.hasPermission("core.user.revoke_other_token")
) && <Button
@@ -146,7 +146,7 @@ export default function UserViewPage() {
<DeleteOutlined />
</Button>
</Popconfirm>}
</>}
</Space>}
/>
<Form
form={form}
@@ -249,6 +249,7 @@ export default function UsersTable({ instanceId, onlyOnline = false, pagination,
rowKey={(user) => user.name}
pagination={defaultPagination}
size={size}
scroll={{ x: "max-content" }}
onRow={(user) => ({
onClick: (event) => {
if ((event.target as HTMLElement).closest("a")) {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clusterio/plugin-global_chat",
"version": "2.0.0-alpha.22",
"version": "2.0.0-alpha.22b",
"description": "Clusterio plugin forwarding between Factorio servers",
"author": "Hornwitser <github@hornwitser.no>",
"homepage": "https://github.com/clusterio/clusterio#readme",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clusterio/plugin-inventory_sync",
"version": "2.0.0-alpha.22",
"version": "2.0.0-alpha.22b",
"description": "Clusterio plugin forwarding between Factorio servers",
"author": "Danielv <danielv@danielv.no>",
"homepage": "https://github.com/clusterio/factorioClusterio#readme",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clusterio/plugin-player_auth",
"version": "2.0.0-alpha.22",
"version": "2.0.0-alpha.22b",
"description": "Clusterio plugin authenticating logged in players to the web interface",
"author": "Hornwitser <github@hornwitser.no>",
"homepage": "https://github.com/clusterio/clusterio#readme",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clusterio/plugin-research_sync",
"version": "2.0.0-alpha.22",
"version": "2.0.0-alpha.22b",
"description": "Clusterio plugin for synchronizing research between Factorio servers",
"author": "Hornwitser <github@hornwitser.no>",
"homepage": "https://github.com/clusterio/clusterio#readme",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clusterio/plugin-statistics_exporter",
"version": "2.0.0-alpha.22",
"version": "2.0.0-alpha.22b",
"description": "Clusterio plugin for gathering statistics from Factorio servers",
"author": "Hornwitser <github@hornwitser.no>",
"homepage": "https://github.com/clusterio/clusterio#readme",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clusterio/plugin-subspace_storage",
"version": "2.0.0-alpha.22",
"version": "2.0.0-alpha.22b",
"description": "Clusterio plugin for sharing storage between Factorio servers",
"author": "Hornwitser <github@hornwitser.no>",
"homepage": "https://github.com/clusterio/clusterio#readme",
+13
View File
@@ -74,6 +74,19 @@ describe("controller/src/routes", function() {
assert.equal(await responses[1].text(), "test content");
});
});
describe("/api/cluster-name", function() {
let endpoint;
beforeEach(function() {
endpoint = `http://localhost:${port}/api/cluster-name`;
});
it("should return the cluster name from config", async function() {
controller.mockConfigEntries.set("controller.name", "Test Cluster");
let response = await fetch(endpoint);
assert.equal(response.status, 200);
let data = await response.json();
assert.deepEqual(data, { name: "Test Cluster" });
});
});
describe("/api/upload-save", function() {
let endpoint;
beforeEach(function() {