Merge branch 'explosivegaming:main' into translate

This commit is contained in:
phidias authored and GitHub committed 2026-10-17 02:55:57 +09:00
commit 95b5c8679d
30 files changed
+27 -196

No files matched your search

+4 -1
View File
@@ -29,7 +29,7 @@ This is a plugin collection for [Clusterio](https://github.com/clusterio/cluster
To use this plugin you must have a clusterio instance running, see [here](https://github.com/clusterio/clusterio?tab=readme-ov-file#installation) for clusterio installation instructions.
This module is currently not published and therefore can not be installed via `npm`. Instead follow the steps for [building from source](#building-from-source)
These plugins can then be installed via `npm install` or using `npx clusteriocontroller plugin install`. See [our plugin list](#plugins) for all the plugins which are available.
## Building from source
@@ -73,6 +73,7 @@ For the locale, access to [Translation](https://translate.aperx.org).
| Release* | Release Name | Factorio Version** |
|---|---|---|
| [7.0.2][s7.0.2] | Clusterio Plugin (Release) | [2.1.22][f2.1.22] |
| [7.0][s7.0] | Clusterio Plugin (Beta) | [2.1.7][f2.1.7] |
| [6.4][s6.4] | Farewell Factorio 1.1 | [1.1.110][f1.1.110] |
| [6.3][s6.3] | Feature Bundle 2: Electric Boogaloo | [1.1.101][f1.1.101] |
@@ -100,6 +101,7 @@ For the locale, access to [Translation](https://translate.aperx.org).
\*\* Factorio versions show the version they were made for, often the minimum requirement to run the scenario.
[s7.0.2]: https://github.com/explosivegaming/scenario/releases/tag/7.0.2
[s7.0]: https://github.com/explosivegaming/scenario/releases/tag/7.0.0
[s6.4]: https://github.com/explosivegaming/scenario/releases/tag/6.4.0
[s6.3]: https://github.com/explosivegaming/scenario/releases/tag/6.3.0
@@ -123,6 +125,7 @@ For the locale, access to [Translation](https://translate.aperx.org).
[s1.0]: https://github.com/explosivegaming/scenario/releases/tag/v1.0
[s0.1]: https://github.com/explosivegaming/scenario/releases/tag/v0.1
[f2.1.22]: https://wiki.factorio.com/Version_history/2.1.0#2.1.22
[f2.1.7]: https://wiki.factorio.com/Version_history/2.1.0#2.1.7
[f1.1.110]: https://wiki.factorio.com/Version_history/1.1.0#1.1.110
[f1.1.101]: https://wiki.factorio.com/Version_history/1.1.0#1.1.101
+1 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@expcluster/auto-restart",
"version": "7.0.1",
"version": "7.0.2",
"description": "Clusterio plugin restarting hosts and the controller once no players are online.",
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
"license": "MIT",
@@ -17,15 +17,11 @@
},
"peerDependencies": {
"@clusterio/controller": "workspace:^",
"@clusterio/host": "workspace:^",
"@clusterio/lib": "workspace:^"
},
"peerDependenciesMeta": {
"@clusterio/controller": {
"optional": true
},
"@clusterio/host": {
"optional": true
}
},
"devDependencies": {
+2 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@expcluster/lib_commands",
"version": "7.0.1",
"version": "7.0.2",
"description": "Clusterio plugin providing a Lua command processing library.",
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
"license": "MIT",
@@ -14,19 +14,16 @@
"node": ">=22"
},
"peerDependencies": {
"@clusterio/controller": "workspace:^",
"@clusterio/host": "workspace:^",
"@clusterio/lib": "workspace:^"
},
"peerDependenciesMeta": {
"@clusterio/controller": {
"optional": true
},
"@clusterio/host": {
"optional": true
}
},
"devDependencies": {
"@clusterio/host": "workspace:^",
"@clusterio/lib": "workspace:^",
"@types/node": "catalog:",
"typescript": "catalog:",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@expcluster/discord-auth",
"version": "7.0.0",
"version": "7.0.2",
"description": "Clusterio plugin linking Factorio accounts to Discord accounts and logging in to the web interface with Discord",
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@expcluster/permission-groups",
"version": "7.0.1",
"version": "7.0.2",
"description": "Clusterio plugin providing syncing of permission groups",
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
"license": "MIT",
+2 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@expcluster/lib_gui",
"version": "7.0.1",
"version": "7.0.2",
"description": "Clusterio plugin providing a Lua GUI definition library.",
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
"license": "MIT",
@@ -14,19 +14,16 @@
"node": ">=22"
},
"peerDependencies": {
"@clusterio/controller": "workspace:^",
"@clusterio/host": "workspace:^",
"@clusterio/lib": "workspace:^"
},
"peerDependenciesMeta": {
"@clusterio/controller": {
"optional": true
},
"@clusterio/host": {
"optional": true
}
},
"devDependencies": {
"@clusterio/host": "workspace:^",
"@clusterio/lib": "workspace:^",
"@types/node": "catalog:",
"typescript": "catalog:",
@@ -63,5 +63,3 @@ NetPlayTime=Net Play Time
NetPlayTime-tooltip=Net Play Time
AFKTimeRatio=AFK Time Ratio
AFKTimeRatio-tooltip=AFK Time over Play Time
Locale=Locale
Locale-tooltip=The language of player interface
@@ -63,5 +63,3 @@ NetPlayTime=淨遊玩時間
NetPlayTime-tooltip=遊玩時間減掛機時間
AFKTimeRatio=掛機時間比
AFKTimeRatio-tooltip=掛機時間除以遊玩時間
Locale=語言
Locale-tooltip=用戶介面語言
@@ -63,5 +63,3 @@ NetPlayTime=淨遊玩時間
NetPlayTime-tooltip=遊玩時間減掛機時間
AFKTimeRatio=掛機時間比
AFKTimeRatio-tooltip=掛機時間除以遊玩時間
Locale=語言
Locale-tooltip=用戶介面語言
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@expcluster/player-statistics",
"version": "7.0.1",
"version": "7.0.2",
"description": "Clusterio plugin keeping player statistics on the controller",
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@expcluster/reports",
"version": "7.0.1",
"version": "7.0.2",
"description": "Clusterio plugin storing player reports on the controller",
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@expcluster/roles",
"version": "7.0.1",
"version": "7.0.2",
"description": "Clusterio plugin providing syncing of in game roles.",
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
"license": "MIT",
-1
View File
@@ -85,7 +85,6 @@ It can be run again. Roles that already exist by name keep their permissions and
The controller keeps some settings per player in `database/exp_scenario/players.json`, so they follow a player between servers:
- Join message. Printed to everyone when the player joins, unless `greetings` is disabled. Set in game with `/set-join-message` and removed with `/remove-join-message`, or edited on the user page of the web interface. Up to 255 characters.
- Locale. Sent when the player joins and when they change language, shown on the user page.
- Toolbar. The order and state of the player's toolbar buttons, sent when they leave and restored when they join.
## Permissions
+4 -8
View File
@@ -25,18 +25,18 @@ export default async function (context: ControllerPluginContext) {
).bootstrap()
);
type PlayerSettings = Pick<messages.PlayerRecord, "joinMessage" | "locale" | "toolbar">;
type PlayerSettings = Pick<messages.PlayerRecord, "joinMessage" | "toolbar">;
/** Change some settings of a player, empty values are removed and so is a record left empty */
function updatePlayer(name: string, changes: PlayerSettings) {
const old = playerStore.get(name);
const settings: PlayerSettings = {
joinMessage: old?.joinMessage, locale: old?.locale, toolbar: old?.toolbar, ...changes,
joinMessage: old?.joinMessage, toolbar: old?.toolbar, ...changes,
};
const record = new messages.PlayerRecord(
name, settings.joinMessage || undefined, settings.locale || undefined, settings.toolbar || undefined,
name, settings.joinMessage || undefined, settings.toolbar || undefined,
);
if (record.joinMessage || record.locale || record.toolbar) {
if (record.joinMessage || record.toolbar) {
playerStore.set(record);
} else if (old) {
playerStore.delete(old);
@@ -234,10 +234,6 @@ export default async function (context: ControllerPluginContext) {
updatePlayer(request.playerName, { joinMessage: request.message?.trim() });
});
controller.handle(messages.PlayerLocaleEvent, async (event: messages.PlayerLocaleEvent) => {
updatePlayer(event.playerName, { locale: event.locale });
});
controller.handle(messages.PlayerToolbarEvent, async (event: messages.PlayerToolbarEvent) => {
updatePlayer(event.playerName, { toolbar: event.toolbar });
});
-1
View File
@@ -27,7 +27,6 @@ export const plugin: lib.PluginDeclaration = {
messages.FeatureUpdateRequest,
messages.PlayerGetRequest,
messages.JoinMessageSetRequest,
messages.PlayerLocaleEvent,
messages.PlayerToolbarEvent,
],
-6
View File
@@ -7,8 +7,6 @@ import { features, FeatureValue } from "./features.js";
export type IpcJoinMessageSet = { player_name: string, message?: string };
/** Sent by the lua side when a player asks for their join message */
export type IpcJoinMessageGet = { player_name: string };
/** Sent by the lua side when a player joins or changes language, see module/control/player_settings.lua */
export type IpcLocale = { player_name: string, locale: string };
/** Sent by the lua side when a player leaves */
export type IpcToolbar = { player_name: string, toolbar: string };
@@ -98,10 +96,6 @@ export default async function (context: InstancePluginContext) {
);
});
instance.server.handle("exp_scenario:locale", async (event: IpcLocale) => {
instance.sendTo("controller", new messages.PlayerLocaleEvent(event.player_name, event.locale));
});
instance.server.handle("exp_scenario:toolbar", async (event: IpcToolbar) => {
instance.sendTo("controller", new messages.PlayerToolbarEvent(event.player_name, event.toolbar));
});
-32
View File
@@ -1,32 +0,0 @@
/**
* Country of each Factorio locale without a region in its code, used for the
* flag. Languages spoken across many countries or without one, such as ar,
* ca, eo, and eu, are left out and get no flag.
*/
const localeCountries: Record<string, string> = {
af: "ZA", be: "BY", bg: "BG", cs: "CZ", da: "DK", de: "DE", el: "GR", en: "GB", et: "EE",
fa: "IR", fi: "FI", fil: "PH", fr: "FR", he: "IL", hr: "HR", hu: "HU", id: "ID", is: "IS",
it: "IT", ja: "JP", ka: "GE", kk: "KZ", ko: "KR", lt: "LT", lv: "LV", nl: "NL", no: "NO",
pl: "PL", ro: "RO", ru: "RU", sk: "SK", sl: "SI", sq: "AL", sr: "RS", th: "TH", tr: "TR",
uk: "UA", vi: "VN",
};
/** Flag emoji for a Factorio locale such as "de" or "pt-BR", empty when it has no obvious country. */
export function localeFlag(locale: string) {
const [language, region] = locale.split("-");
const country = region ?? localeCountries[language];
if (!country || !/^[A-Z]{2}$/.test(country)) {
return "";
}
// Regional indicator symbols start at U+1F1E6 for A
return String.fromCodePoint(...[...country].map(letter => 0x1F1E6 + letter.charCodeAt(0) - 65));
}
/** English name of a locale, the code itself when it is not known. */
export function localeName(locale: string) {
try {
return new Intl.DisplayNames(["en"], { type: "language" }).of(locale) ?? locale;
} catch {
return locale;
}
}
-34
View File
@@ -134,8 +134,6 @@ export class PlayerRecord {
public id: string,
/** Shown to everyone when the player joins */
public joinMessage?: string,
/** The language of their game, such as "en" or "pt-BR" */
public locale?: string,
/** Toolbar state from Gui.toolbar.get_state, json compressed with helpers.encode_string */
public toolbar?: string,
public updatedAtMs: number = 0,
@@ -145,7 +143,6 @@ export class PlayerRecord {
static jsonSchema = Type.Object({
id: Type.String(),
join_message: Type.Optional(Type.String()),
locale: Type.Optional(Type.String()),
toolbar: Type.Optional(Type.String()),
updated_at_ms: Type.Optional(Type.Number()),
is_deleted: Type.Optional(Type.Boolean()),
@@ -156,9 +153,6 @@ export class PlayerRecord {
if (this.joinMessage) {
json.join_message = this.joinMessage;
}
if (this.locale) {
json.locale = this.locale;
}
if (this.toolbar) {
json.toolbar = this.toolbar;
}
@@ -175,7 +169,6 @@ export class PlayerRecord {
return new this(
json.id,
json.join_message,
json.locale,
json.toolbar,
json.updated_at_ms ?? 0,
json.is_deleted ?? false,
@@ -257,33 +250,6 @@ export class JoinMessageSetRequest {
}
}
/** The language a player's game uses, sent when they join and when they change it. */
export class PlayerLocaleEvent {
declare ["constructor"]: typeof PlayerLocaleEvent;
static plugin = "exp_scenario" as const;
static type = "event" as const;
static src = "instance" as const;
static dst = "controller" as const;
constructor(
public playerName: string,
public locale: string,
) {}
static jsonSchema = Type.Object({
player_name: Type.String(),
locale: Type.String(),
});
toJSON() {
return { player_name: this.playerName, locale: this.locale };
}
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.player_name, json.locale);
}
}
/** The toolbar of a player, sent when they leave. */
export class PlayerToolbarEvent {
declare ["constructor"]: typeof PlayerToolbarEvent;
@@ -8,12 +8,6 @@ calls receive_toolbar with it when they join again
local Gui = require("modules/exp_gui")
local clusterio_api = require("modules/clusterio/api")
--- @param event EventData.on_player_joined_game | EventData.on_player_locale_changed
local function send_locale(event)
local player = assert(game.get_player(event.player_index))
clusterio_api.send_json("exp_scenario:locale", { player_name = player.name, locale = player.locale })
end
--- @param event EventData.on_player_left_game
local function send_toolbar(event)
local player = assert(game.get_player(event.player_index))
@@ -60,8 +54,6 @@ local e = defines.events
return {
events = {
[e.on_player_joined_game] = send_locale,
[e.on_player_locale_changed] = send_locale,
[e.on_player_left_game] = send_toolbar,
},
receive_toolbar = receive_toolbar,
-6
View File
@@ -80,12 +80,6 @@ local computed_stats = {
return format_number_2dp(get_stat(player, "AfkTime", 0) * 100 / get_stat(player, "Playtime", 1))
end,
},
Locale = {
default = "en",
calculate = function(player)
return player.locale
end,
},
}
--- Label used for all data in the data table
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@expcluster/scenario",
"version": "7.0.1",
"version": "7.0.2",
"description": "Clusterio plugin implementing the Explosive Gaming scenario.",
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
"license": "MIT",
-12
View File
@@ -278,18 +278,6 @@ t.test("player settings", t2 => {
t3.equal(record.joinMessage, "Bye", "kept across restarts");
});
t2.test("PlayerLocaleEvent stores the locale next to the join message", async t3 => {
const { send } = await startPlugin(t3, { withPlugins: false });
await send(new messages.JoinMessageSetRequest("Alice", "Hi"));
await send(new messages.PlayerLocaleEvent("Alice", "pt-BR"));
const record = await send(new messages.PlayerGetRequest("Alice"));
t3.strictSame([record.joinMessage, record.locale], ["Hi", "pt-BR"]);
await send(new messages.JoinMessageSetRequest("Alice", null));
const cleared = await send(new messages.PlayerGetRequest("Alice"));
t3.equal(cleared.locale, "pt-BR", "clearing the message keeps the locale");
});
t2.test("PlayerToolbarEvent stores the toolbar and it is saved", async t3 => {
const databaseDirectory = t3.testdir();
const { send, save } = await startPlugin(t3, { withPlugins: false, databaseDirectory });
+1 -8
View File
@@ -196,7 +196,7 @@ t.test("player settings", t2 => {
});
t2.test("a joining player's stored toolbar is sent to the lua side", async t3 => {
const bob = new messages.PlayerRecord("Bob", undefined, undefined, "eNqrVkrLz1eyUlBKSixSqgUAGQwD8g==");
const bob = new messages.PlayerRecord("Bob", undefined, "eNqrVkrLz1eyUlBKSixSqgUAGQwD8g==");
const { start, playerEvent, setStatus, state } = await startPlugin(t3, [], [bob]);
await start();
setStatus("running");
@@ -216,13 +216,6 @@ t.test("player settings", t2 => {
t3.strictSame([event.playerName, event.toolbar], ["Alice", "abc"]);
});
t2.test("ipc with a locale is forwarded to the controller", async t3 => {
const { ipc, state } = await startPlugin(t3);
await ipc("exp_scenario:locale", { player_name: "Alice", locale: "de" });
const [event] = state.sent.filter(request => request instanceof messages.PlayerLocaleEvent);
t3.strictSame([event.playerName, event.locale], ["Alice", "de"]);
});
t2.test("ipc asking for the message prints it to the player", async t3 => {
const { ipc, setStatus, state } = await startPlugin(t3, [], [alice]);
setStatus("running");
-26
View File
@@ -1,26 +0,0 @@
import t from "tap";
import { localeFlag, localeName } from "../dist/node/locales.js";
// The locale folders of Factorio 2.1
const factorioLocales = [
"af", "ar", "be", "bg", "ca", "cs", "da", "de", "el", "en", "eo", "es-ES", "et", "eu", "fa", "fi", "fil", "fr",
"fy-NL", "ga-IE", "he", "hr", "hu", "id", "is", "it", "ja", "ka", "kk", "ko", "lt", "lv", "nl", "no", "pl",
"pt-BR", "pt-PT", "ro", "ru", "sk", "sl", "sq", "sr", "sv-SE", "th", "tr", "uk", "vi", "zh-CN", "zh-TW",
];
const noFlag = ["ar", "ca", "eo", "eu"];
t.test("localeFlag()", async t2 => {
t2.equal(localeFlag("de"), "\u{1F1E9}\u{1F1EA}", "language codes use the table");
t2.equal(localeFlag("pt-BR"), "\u{1F1E7}\u{1F1F7}", "regions are used directly");
t2.equal(localeFlag("zh-TW"), "\u{1F1F9}\u{1F1FC}");
for (const locale of factorioLocales) {
t2.equal(localeFlag(locale) === "", noFlag.includes(locale), `${locale} has a flag unless it has no country`);
}
t2.equal(localeFlag("xx"), "", "unknown languages have no flag");
t2.equal(localeFlag(""), "");
});
t.test("localeName()", async t2 => {
t2.equal(localeName("de"), "German");
t2.equal(localeName("not a locale"), "not a locale", "invalid codes are shown as they are");
});
+1 -8
View File
@@ -9,7 +9,7 @@ local valid_state = { order = { { name = "readme", favourite = true } }, open =
--- @return table restored Calls to set_state as { player name, state }
local function load(env, toolbar)
local restored = {}
env.extend_defines{ events = { on_player_locale_changed = 3, on_player_left_game = 4 } }
env.extend_defines{ events = { on_player_left_game = 4 } }
env.extend_requires{
["modules/exp_gui"] = {
toolbar = toolbar or {
@@ -28,13 +28,6 @@ local function load(env, toolbar)
return env.load_module("control/player_settings"), restored
end
Suite.test("send_locale() sends the locale of the player", function(env)
local PlayerSettings = load(env)
rawset(env.add_player{ name = "Alice" }, "locale", "pt-BR")
PlayerSettings.events[defines.events.on_player_joined_game]{ player_index = 1 }
Suite.eq(env.sent, { { channel = "exp_scenario:locale", data = { player_name = "Alice", locale = "pt-BR" } } }, "sent")
end)
Suite.test("send_toolbar() sends the compressed toolbar when a player leaves", function(env)
local PlayerSettings = load(env)
env.add_player{ name = "Alice" }
+1 -1
View File
@@ -1,4 +1,4 @@
{
"extends": "../tsconfig.browser.json",
"include": [ "web/**/*.tsx", "web/**/*.ts", "index.ts", "messages.ts", "features.ts", "locales.ts", "permissions.ts", "package.json" ],
"include": [ "web/**/*.tsx", "web/**/*.ts", "index.ts", "messages.ts", "features.ts", "permissions.ts", "package.json" ],
}
@@ -4,7 +4,6 @@ import { Button, Descriptions, Input, Space } from "antd";
import { ControlContext, PluginExtensionProps, SectionHeader, notifyErrorHandler, useAccount } from "@clusterio/web_ui";
import { JoinMessageSetRequest, PlayerGetRequest, PlayerRecord, joinMessageMaxLength } from "../../messages.js";
import { localeFlag, localeName } from "../../locales.js";
/** The settings the scenario keeps for a player, shown on their user page. */
export default function UserPlayerSettings(props: PluginExtensionProps<"UserViewPage">) {
@@ -49,11 +48,6 @@ export default function UserPlayerSettings(props: PluginExtensionProps<"UserView
return <>
<SectionHeader title="Scenario" />
<Descriptions bordered size="small" column={1}>
<Descriptions.Item label="Language">
{record.locale
? `${localeFlag(record.locale)} ${localeName(record.locale)} (${record.locale})`.trim()
: <span style={{ color: "#888" }}>Unknown</span>}
</Descriptions.Item>
<Descriptions.Item label="Join message">
{draft !== undefined
? <Space.Compact style={{ width: "100%", maxWidth: 600 }}>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@expcluster/server-list",
"version": "7.0.1",
"version": "7.0.2",
"description": "Clusterio plugin sharing the details of every instance with the others.",
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
"license": "MIT",
+2 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@expcluster/server-ups",
"version": "7.0.1",
"version": "7.0.2",
"description": "Clusterio plugin providing in game server ups counter",
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
"license": "MIT",
@@ -14,19 +14,16 @@
"node": ">=22"
},
"peerDependencies": {
"@clusterio/controller": "workspace:^",
"@clusterio/host": "workspace:^",
"@clusterio/lib": "workspace:^"
},
"peerDependenciesMeta": {
"@clusterio/controller": {
"optional": true
},
"@clusterio/host": {
"optional": true
}
},
"devDependencies": {
"@clusterio/host": "workspace:^",
"@clusterio/lib": "workspace:^",
"typescript": "catalog:",
"@types/node": "catalog:",
+2 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@expcluster/lib_util",
"version": "7.0.1",
"version": "7.0.2",
"description": "Clusterio plugin providing Lua libraries and other utilities.",
"author": "Cooldude2606 <https://github.com/Cooldude2606>",
"license": "MIT",
@@ -14,19 +14,16 @@
"node": ">=22"
},
"peerDependencies": {
"@clusterio/controller": "workspace:^",
"@clusterio/host": "workspace:^",
"@clusterio/lib": "workspace:^"
},
"peerDependenciesMeta": {
"@clusterio/controller": {
"optional": true
},
"@clusterio/host": {
"optional": true
}
},
"devDependencies": {
"@clusterio/host": "workspace:^",
"@clusterio/lib": "workspace:^",
"@types/node": "catalog:",
"typescript": "catalog:",