Save both datastores on the save hook so autosave covers them, the
controller also runs it at shutdown. Watch list names are case
insensitive, matching Factorio and Clusterio users. The new
exp_reports.announce_to_all_instances config, on by default, can limit
the join message to the instance the player joined. The Discord embed
lists the three newest unread reports and counts the rest in the footer,
since Discord rejects embeds with more than 25 fields. Both tables now
sort newest first by default.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Players on the watch list are announced to every online admin on every
instance when they join, and the join is posted to the Discord and JSON
webhooks. The Reports page has a second table for the watch list with an
Add button and a Delete button per row.
Reports can be marked read or unread from the web interface. Each report
row also has an Add to watch list button which opens the add form filled
in with the player and reason. Setting exp_reports.watch_unread_reports
treats players with unread reports as watched.
Reports now store created_at_ms because marking them read changes
updated_at_ms. Old reports.json files migrate on load with updated_at_ms
as the creation time. The report webhook moved from the datastore update
listener to the create handler so reading a report is not posted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
utility/downloading_white was a 1.1 sprite. Neither 2.0 nor 2.1 has it,
so drawing the readme for a modded server crashed on_player_created.
utility/downloading exists in both, so no version check is needed.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Commands, Permission Groups, Player Statistics, Reports, Roles, Scenario,
Server UPS and Module Utilities get a readme covering their config,
permissions, commands, web pages and Lua and Node API.
The existing readmes are brought up to date: Auto Restart now that
clusterio marks a restart after a plugin install, Discord Auth gets the
code timeout, command aliases and its Lua API, and the GUI guide says what
the plugin is and has its toolbar examples and an iterator name fixed.
The main readme gets a Plugins section listing each plugin by name and
package with a one line description.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
exp_reports, exp_roles and exp_groups wrapped the json they send to Lua in
a fixed [=[ ]=] long bracket. A report reason, role name or group name
containing ]=] closed it early and the rest ran as Lua through /sc. Any
player can make a report, so any player could run code on the server.
Use the next level the json does not contain, like the other plugins do.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The plugin is now exp_player_statistics (@expcluster/player-statistics),
with the module, global, permission, route and database directory
renamed to match. The exp-statistics locale section keeps its name.
Every statistic is tracked unconditionally, so the enable flags are gone
from config.lua. Playtime and JoinCount are no longer counted in game:
clusterio already keeps online time and joins for each user, so the
controller fills them in from the user when a player's totals are
requested, and the web page does the same from the user list. They are
not stored or adjusted by the plugin.
The web table has a column picker, a multiple select with check boxes,
Select all and Reset buttons, showing "N statistics selected". The
choice is kept in local storage. The user page shows the statistics of
the player in a grid like the core player stats.
The readme statistics table drops its second title and uses the short
time format.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tab listed the legacy player data, its required, settings and misc
groups and the data saving preference. None of that is synced or
relevant any more, so the tab is now Statistics and lists the totals
from exp_statistics in display order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Remove the tracking and config from exp_legacy along with its
exp-statistics locale, which now ships with exp_statistics.
The player stats GUI stays in exp_scenario and reads the new module. It
uses the online player dropdown from exp_scenario's elements, so moving
it would make exp_statistics depend on exp_scenario, and its
exp_scenario.gui.playerdata permission is already in the seeded roles.
The readme data tab shows the statistics from the new module above the
remaining PlayerData.Statistics children, which is only LocalLanguage
now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Player statistics move out of exp_legacy into their own plugin, with the
totals kept on the controller instead of the PlayerData datastore, which
was never synced anywhere.
The lua module tracks the same statistics with the same rules. Gains are
kept per player as unsent in storage and sent over IPC every 5 minutes
for every player with changes, and for one player when they leave. Sent
values move into the totals the controller gave when the player joined,
so get() is the total on every server either way.
Nothing is resent. The instance plugin sends everything left when the
server stops and drops anything unsent when it starts. A leftover can
only come from a save made before it was sent, such as the autosave
loaded after a crash, so dropping it loses a few minutes at worst but
never counts twice.
The controller adds the deltas to a record per player name in
database/exp_statistics/players.json. Instances fetch a player's totals
on join and hand them to lua over rcon. The web UI gets a sortable table
of every player at /exp_statistics, behind exp_statistics.view.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Add a community_details feature for the community name and links
The community name, Discord, website, status, GitHub, and Patreon links
lived in exp_legacy/config/server_detail.lua, so changing them meant
editing the scenario. They are now the community_details feature and can
be edited from the Scenario Features page.
Chat auto reply, help bubbles, and the readme read the live config
instead of copying the values when the file loads, so an edit shows up
without a restart. Help bubbles store the location name in their task
and build the messages each cycle; the readme tab tooltip can be a
function which is called when the readme opens.
Every feature gets an enabled switch from the framework. There is no
sensible meaning for a disabled community name, so the values are
always used and the feature description says the switch does nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Move greetings to exp_scenario and drop preset colours
The greeting reads the community name and Discord link from the new
community_details feature, which exp_legacy can not depend on, so it moves
to exp_scenario/control/greetings.lua along with its commands and
locale. The custom message still lives in PlayerData for now.
The hardcoded per-player join messages in config/join_messages.lua are
removed with their fallback, as are the preset player colours. Player
sync already restores the colour of returning players, so new players
keep the colour Factorio gives them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
The greeting reads the community name and Discord link from the new
community_details feature, which exp_legacy can not depend on, so it moves
to exp_scenario/control/greetings.lua along with its commands and
locale. The custom message still lives in PlayerData for now.
The hardcoded per-player join messages in config/join_messages.lua are
removed with their fallback, as are the preset player colours. Player
sync already restores the colour of returning players, so new players
keep the colour Factorio gives them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The community name, Discord, website, status, GitHub, and Patreon links
lived in exp_legacy/config/server_detail.lua, so changing them meant
editing the scenario. They are now the community_details feature and can
be edited from the Scenario Features page.
Chat auto reply, help bubbles, and the readme read the live config
instead of copying the values when the file loads, so an edit shows up
without a restart. Help bubbles store the location name in their task
and build the messages each cycle; the readme tab tooltip can be a
function which is called when the readme opens.
Every feature gets an enabled switch from the framework. There is no
sensible meaning for a disabled community name, so the values are
always used and the feature description says the switch does nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The controller and instance plugins are init functions with closures.
The controller batches refreshes with setMany and deleteMany, and the
instance gates updates on its status instead of a started flag.
short_name is now display_name. New always_on_top field sorts servers
first in get_servers. The record carries has_password from the game
password in factorio.settings, never the password itself, and the
description falls back to the one in factorio.settings. A controller
welcome is used when the instance has none, and the readme falls back
to a default welcome from the locale.
Lua gets is_synced for connect.lua. The readme passes an empty detail
instead of nil to the join tooltip so the player count stays in __2__,
restores the Password status, shows the welcome title as a centered
label and shortens long descriptions with the full text as a tooltip.
The Lua tests assert the lookups they index, which fixes the lint.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The controller keeps a record per instance with its name, mod pack,
Factorio version, players online, status and address, and the instance
plugin pushes the list into lua over rcon. The readme and the connect
commands read it through modules/exp_server_list, so expcore/external
and its hardcoded server fallback are removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Add exp_discord plugin for Discord login and account linking
Players log in to the web interface with Discord through OAuth2 with the
identify scope, so no bot token is needed. After logging in they get a
code from the login page and type /link-discord <code> in game to link
that Factorio account. One Discord account can hold up to
exp_discord.max_accounts Factorio accounts (default 3), and linking a
name moves it off any other Discord account.
Sessions and link codes live in memory on the controller plugin, the
links are stored in database/exp_discord/accounts.json. The user page
shows the linked Discord account and lets the user, or anyone with
exp_discord.link.delete, unlink it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Link Discord from the user page and document the setup
The user page gets a Link Discord button. It asks the controller for a
one-time ticket bound to the logged in user, then goes through the
authorize route with it. The callback links the Discord account to that
user directly, without the in-game code, and goes back to the user page.
The link flow always shows the Discord consent screen.
The README covers registering the redirect URL and the config.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Address review of the Discord plugin
The plugin is now exp_discord_auth, so later Discord plugins can sit
next to it. Config keys, permissions, routes and the Lua module moved
with it, and the Discord redirect is now api/exp_discord_auth/callback.
The controller registers its handlers and routes in the constructor and
uses the express types, the instance plugin is a closure, and the Lua
commands file returns its commands. The get and unlink responses carry
the controller url so the in game messages can name the website.
The login form asks the new status route whether Discord is configured
and shows the error straight away instead of a button that fails. The
button is full width like the link button, and a "Not you?" link next
to the Discord name replaces the separate switch account button.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The plugin is now exp_discord_auth, so later Discord plugins can sit
next to it. Config keys, permissions, routes and the Lua module moved
with it, and the Discord redirect is now api/exp_discord_auth/callback.
The controller registers its handlers and routes in the constructor and
uses the express types, the instance plugin is a closure, and the Lua
commands file returns its commands. The get and unlink responses carry
the controller url so the in game messages can name the website.
The login form asks the new status route whether Discord is configured
and shows the error straight away instead of a button that fails. The
button is full width like the link button, and a "Not you?" link next
to the Discord name replaces the separate switch account button.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The user page gets a Link Discord button. It asks the controller for a
one-time ticket bound to the logged in user, then goes through the
authorize route with it. The callback links the Discord account to that
user directly, without the in-game code, and goes back to the user page.
The link flow always shows the Discord consent screen.
The README covers registering the redirect URL and the config.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Players log in to the web interface with Discord through OAuth2 with the
identify scope, so no bot token is needed. After logging in they get a
code from the login page and type /link-discord <code> in game to link
that Factorio account. One Discord account can hold up to
exp_discord.max_accounts Factorio accounts (default 3), and linking a
name moves it off any other Discord account.
Sessions and link codes live in memory on the controller plugin, the
links are stored in database/exp_discord/accounts.json. The user page
shows the linked Discord account and lets the user, or anyone with
exp_discord.link.delete, unlink it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Inventory sync already stores the player colour, tag, quickbar and alt
view (game view settings), so the legacy alt-view, quickbar and tag
modules and the colour persistence are removed along with
/save-quickbar and its quickbar presets.
The /tag, /tag-color and /tag-clear commands move to exp_scenario and
write player.tag directly, which inventory sync then carries between
servers. /tag-color reads the current text back out of the tag, so it
now needs a tag to be set first. It also used to concatenate the colour
table into the tag and error, it now formats it as rich text.
player-colours keeps giving new players their preset or a random
colour on creation, inventory sync replaces it with the stored one for
anyone who has played before.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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>