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>
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>
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>
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>
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>
Restarts hosts, and optionally the controller, that report a pending
restart once no players have been online for a configurable time. Only
loaded on the controller: it counts players from the users online on
running instances, sends HostRestartRequest through the existing core
path, and starts the instances that were running once the host reports
back. Each host process is asked at most once, keyed on its process
start time, so a stale system info poll cannot trigger a second restart.
Uses the function-style entrypoint clusterio now documents, so it loads
without the deprecated class export warning.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Reports were the last legacy control module, kept in the lua state of
each instance. They are now an exp_reports plugin which stores them on
the controller: a report is immutable and holds the reported player, who
made it, the reason, the instance name and when it was made, and the only
operations are get, list, create and delete.
In game nothing is stored. The lua module sends each request over ipc,
the instance plugin asks the controller and hands the answer back with
rcon, and the module prints it to whoever asked if they are still online.
Answers are dropped if the instance stopped meanwhile. The module raises
on_player_reported with the count and reporters of every report against
the player, which report_jail and discord_alerts use, and the commands
and player list button call into it.
The web ui gets a /reports page: a table with search on the player,
reporter and reason, a filter on the instance, delete per row and a
create form which reports in the name of the web user.
New reports are posted to two optional webhooks configured on the
controller, a Discord channel hook as an embed and a generic JSON hook.
Failures are logged rather than failing the report.
Tests cover the messages, the controller against a real Controller, the
instance plugin against a real Instance, and the lua module through the
shared harness.
Clusterio already stores roles, the permissions they grant, and which user holds
which role, along with a web UI for all three. What it does not have is the
properties a role only needs in game, or a way for an instance to learn about
any of it, since role and user updates are only sent to control connections.
This plugin fills both gaps. The controller keeps a record per role holding the
order, priority, short hand, tag, colour and auto assign threshold, created
automatically for any role which does not have one. It then rebroadcasts roles
and assignments on its own events so instances can follow them.
The lua module presents the same interface the legacy expcore.roles module did,
so the call sites can be moved over without being rewritten. Permission checks
translate the legacy action strings using the same mapping exp_scenario defines.
Two things replace features the legacy system had:
- Priority replaces disallow. Only the roles with the highest priority a player
holds are considered, so Jail can suppress every other role including the
default one, without needing to take roles away first.
- Assignments made in game are applied locally and then sent to the controller,
which keeps assign_player synchronous for callers. A role which should never
leave this map, such as one earned from time on the map, is assigned with
assign_player_local instead.
Roles earned from online time across the cluster are granted by the controller
from the threshold on the role, using the online time clusterio already tracks.
Nothing requires this plugin yet; moving the call sites off expcore.roles and
removing the legacy config is left for a follow up.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Refactor server ups
* Use catalogs
* Move to own plugin
* Use web config
* Remove External.get_server_ups
* Update workspace version requirement
* Remove need for storage
* Add locale
* Fix CI