15 Commits
Author SHA1 Message Date
BastiaanandClaude Opus 5.5 20ca539af3 Save controller plugin data on the save hook (#513)
exp_groups and exp_roles only saved their datastores on shutdown, and
exp_scenario saved the player store but not the feature store on the
save hook. The controller runs the save hook on its autosave interval,
so a crash or kill lost every change since the controller started.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 21:57:21 +00:00
Cooldude2606 a7e66ccdd4 Remove collection of player locale (#508) 2026-10-16 13:55:06 +01:00
bbassieandClaude Opus 5.5 54941b85aa Sync the toolbar of players through the controller
The toolbar order, favourites, and open panels were saved into
exp_legacy's PlayerData, which was never synced, and the file doing so
was no longer loaded. When a player leaves, the lua side now sends
Gui.toolbar.get_state as json compressed with helpers.encode_string,
and the controller stores the string on the player record.

When the player joins again the instance plugin sends the stored string
over rcon to exp_scenario.player_settings.receive_toolbar, which
decodes it and calls Gui.toolbar.set_state. exp_gui builds the toolbar
in on_player_joined_game, so it exists by the time the rcon arrives.
A string which does not decode, or a state naming buttons which no
longer exist, is logged and ignored rather than raising in the game.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 22:10:31 +00:00
bbassieandClaude Opus 5.5 ebe7504c00 Show the language of players on their user page
exp_legacy recorded each player's locale in PlayerData statistics
which nothing read. The lua side now sends one ipc event with the
player's locale when they join and when they change language, the
instance forwards it, and the controller stores it on the player
record.

The Scenario section of the user page shows the language by name with
a flag made from regional indicator symbols. Locales with a region use
it directly, the rest map to a country through a small table covering
Factorio's locale list. Arabic, Catalan, Basque, and Esperanto have no
single country and get no flag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 22:10:31 +00:00
bbassieandClaude Opus 5.5 6821d62c10 Keep join messages on the controller and print them from the instance
Join messages were stored in exp_legacy's PlayerData, which nothing
ever synced. The controller now keeps a players.json datastore of
per-player settings keyed by player name, starting with the join
message, and saves it on the save and shutdown hooks. Records with
nothing set are removed.

When a player joins, the instance plugin fetches their record and
prints the message to everyone over rcon in the player's colour, so no
lua is needed to show it. The in-game /set-join-message and
/remove-join-message commands send ipc which the instance forwards to
the controller; /set-join-message without a message asks the instance,
which prints the stored message back to the player.

Greetings is now a feature. Disabling it stops the greeting, the
commands, and the printed join messages. The private greeting with the
community name and Discord link is shown to every joining player, even
those with a join message, since one is a welcome for the player and
the other an announcement to everyone else.

Two controller permissions cover the web interface:
exp_scenario.join_message.set for your own message and .set_other for
anyone's, checked against the requesting user the same way
exp_discord_auth checks unlinking. The seed grants them to Supporter
and Moderator alongside the in-game commands. The user page gets a
Scenario section showing the message with an edit button for users who
may change it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 22:10:31 +00:00
BastiaanandClaude Opus 5.5 68a964cf57 Write plugins without state shared elsewhere as functions (#492)
The instance plugins of exp_groups, exp_reports, exp_roles, exp_scenario
and exp_server_ups, and the controller plugins of exp_reports and
exp_scenario, were classes only to hold their handlers. They are now
init functions with closures, like clusterio's own plugins.

The exp_roles and exp_groups controller plugins stay classes because
exp_scenario reaches their datastores through get(controller).
AutoRestart stays a class because its tests step check() with a fake
clock.

Tests spy on handle() and the hooks to call the handlers. The reports
webhook tests stub fetch instead of replacing postWebhook, and the
feature reconcile test writes the stored records to disk before start.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:50:15 +01:00
BastiaanandClaude Opus 5.5 ea9bf1fb78 Construct plugins with a static fromContext (#487)
Each plugin class now has a private constructor and a static async
fromContext(context) that loads its datastores, constructs the instance
and attaches its handlers and hooks. This replaces the public constructor
followed by init(), and AutoRestart's start().

Datastores are loaded before construction and passed to the constructor,
so the definite assignment assertions on them are gone. Tests build the
plugins through fromContext and pass a wrapped logger in the context
instead of overwriting plugin.logger before init().

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 01:19:53 +01:00
bbassieandClaude Fable 5.1 bff6a1c612 Fix stale values, the unsynced page, and three smaller review finds
- The page waited for nothing, so before the subscription delivered
  the records every feature looked enabled with its defaults, and an
  Apply in that window would have written that state. It now shows a
  spinner until synced, like the role page.
- Values stored for a setting which was since renamed, removed, or
  changed type made every later save of that feature fail validation.
  The controller now reconciles the stored records with features.ts on
  start: stale values are dropped with a warning, records of removed
  features are deleted, and new features get their defaults.
- update_config no longer stores overrides for unknown paths, so a
  stale key is warned about once instead of on every load.
- The armor and gun inventory handlers of nuke protection are new to
  this branch and asserted on get_inventory, which is nil without a
  character. They return instead.
- The AFK check ran every 30 minutes while afk_minutes and kick_minutes
  can be set as low as 1. It runs every minute.
- sameValue lives in features.ts and isDefaultValue uses it, and the
  custom input branch of FieldInput is a single narrowed check.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 21:58:25 +00:00
bbassieandClaude Opus 5.5 88358b23b9 Add a web page for configuring scenario features
Features can now be enabled, disabled, and configured from the web UI
while the game is running, without editing Lua files. The first five
features covered are AFK Kick, Chat Auto Reply, Death Markers, Mine
Depletion, and Station Auto Name.

Lua side: module/features.lua holds each feature's defaults, keeps the
overrides from the controller in storage, and patches them onto the
config table in place. Features.guard skips a feature's handlers while
it is disabled. The five config files move from exp_legacy into
exp_scenario/module/config and their control files now read config at
runtime instead of deciding at load which handlers to register. AFK
Kick times are now in minutes.

Controller side: features.ts lists the editable settings and their
defaults. Overrides that differ from the default are stored in
exp_scenario/features.json and guarded by the existing
exp_scenario.config.view/edit permissions. The instance plugin sends
them over rcon on start, on every change, and again after the
controller restarts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:56:14 +00:00
bbassieandClaude Opus 5.5 0ff07c012c Export init functions instead of plugin classes
clusterio logs "using deprecated class export" for every node entrypoint
that exports a Base*Plugin subclass, and the web UI logs "using deprecated
class hooks" for WebPlugin classes. Every entrypoint now has a default
export function, like exp_auto_restart and the exp_roles web plugin.

The classes stay where they hold state, but no longer extend the base
classes. The constructor takes the load context and init() attaches the
hooks the base class used to attach from the on* methods, so the tests can
still call those methods directly. exp_server_ups sends its RCON command
through instance.sendRcon with the plugin name, which is what the removed
BaseInstancePlugin.sendRcon did.

The instance entrypoints of exp_commands, exp_gui, exp_legacy, exp_util
and exp_scenario were empty classes, kept only so the host loads the
plugin and patches its module into the save. An empty function does the
same.

The exp_groups web components read the subscriptions through exported
useGroups, useAssignments and useRoleMappings hooks instead of looking
the plugin up in control.plugins, which only holds class plugins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:10:51 +00:00
Bastiaan 53e07aa761 Merge pull request #481 from bbassie/fix/upstream-plugin-changes
Restore seeding without controller.plugins
2026-09-26 22:48:17 +01:00
bbassieandClaude Fable 5.1 7fd66c6ac7 Migrate to ES modules
Clusterio #984 converted its packages to ESM, so a CommonJS plugin can
no longer import @clusterio/lib and plugin entrypoints must name the
full file. Every package gets "type": "module", entrypoints and relative
imports carry the .js extension, and the webpack configs become .cjs as
webpack-cli loads them with require. tsconfig.node.json moves from
node16 to nodenext to match clusterio, which needs it for the JSON
import attributes in its sources.

The tap tests and the shared test helpers use import and
import.meta.dirname. The test scripts pass --type-strip-only so tap
leaves lib's .ts subpath imports to Node's own type stripping, as its
TypeScript loader compiled them without a default export. The lua
runner resolves fengari from the test file it is given, since a bare
import from the shared test directory does not reach the plugin's
devDependencies.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 01:10:02 +01:00
bbassieandClaude Fable 5.1 30f1b5ea75 Declare permissions on the plugin declaration
Clusterio #988 added a permissions array to PluginDeclaration and a
type level Permissions registry, and made checkPermission and the web
UI hasPermission helpers take PermissionName. Move the definePermission
calls of exp_groups and exp_scenario into the declaration and add the
names to the registry so the web pages type check. The browser
tsconfigs include index.ts so the augmentation is visible to the web
bundle, matching the in-repo plugins.

The exp_scenario table keeps its tuple form and derives the name union
from it, so a new row is still one line. Seed role permissions are typed
as PermissionName, so a typo in seed.ts is now a compile error rather
than a warning at seed time. The tests register the declared
permissions through registerPluginPermissions instead of importing
permissions.ts for its side effect.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 23:27:17 +00:00
bbassieandClaude Fable 5.1 51786c5df9 Export messages and controller types from exp_roles and exp_groups
exp_scenario reached into @expcluster/roles/dist/node/* for the message
classes and the controller plugin type. The package index now re-exports
messages and the ControllerPlugin type, so consumers import from the
package name like they do with @clusterio/lib. The controller export is
type only, which keeps node code out of the web bundle. The test still
requires the controller classes from dist since they cannot be
re-exported at runtime.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 17:31:31 +00:00
Bastiaan 0854a2b7d5 Move the seed into exp_scenario and seed the permission groups (#457)
The seed roles were owned by exp_roles, but they describe the scenario, so
they now live in exp_scenario/seed.ts next to the permissions they grant.
Each seed role names the permission group its holders belong to, and the
five groups the legacy config defined (Admin, Trusted, Standard, Guest,
Restricted) are seeded through exp_groups along with one role mapping per
role. Mapping priorities follow how exp_roles ranks a player's highest
role, so Jail lands in Restricted regardless of other roles.

SeedRolesRequest becomes exp_scenario's SeedRequest behind a new
exp_scenario.seed permission, and the button moves to the scenario's web
plugin, which gets a web entrypoint for it. exp_roles no longer depends on
exp_scenario, and exp_scenario gains controller tests around the seed.

With groups seeded from the controller the legacy expcore.permission_groups
module and its config are removed, along with the Group rcon static.
2026-09-08 11:20:44 +01:00