Commit Graph
191 Commits
Author SHA1 Message Date
Cooldude2606 37cef0e6db Merge branch 'main' into feature/player-settings 2026-10-14 23:39:36 +01:00
Cooldude2606andClaude Opus 5.5 5180cc39d6 Move community details into a scenario feature and drop preset colours (#498)
* 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>
2026-10-14 23:13:33 +01:00
Bastiaan 69e31ac2d9 Merge pull request #500 from bbassie/refactor/settings-in-storage
Keep the server UPS and fast decon toggles out of player data
2026-10-14 23:11:08 +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
bbassieandClaude Opus 5.5 4d3c4de5b1 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>
2026-10-14 22:10:22 +00:00
bbassieandClaude Opus 5.5 4d136853c9 Add exp_server_list to replace expcore/external
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>
2026-10-14 21:35:33 +00:00
BastiaanandClaude Opus 5.5 11ccee572b Drop exp_legacy player data that inventory_sync covers (#491)
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>
2026-10-11 18:29:11 +01:00
Bastiaan 757a36fc03 Merge pull request #486 from bbassie/feature/convert-configs
Convert the remaining scenario configs into features
2026-10-05 00:33:36 +01:00
bbassieandClaude Opus 5.5 4bc1773c82 Address review: page layout, five more features, framework changes
Framework:
- Intervals are gone. Update periods are hardcoded on_nth_tick values
  again, since nobody has needed to change them.
- apply is a single loop over the declared fields and logs a warning
  when an override is invalid. receive_update also warns about
  unknown keys.
- Nested tables are addressed as "section.key". Lists of strings can be
  overridden. Features.optional declares a value with no default.
- Features.on_apply rebuilds values derived from the config, such as
  lookup sets. It also runs during on_load.

exp_roles is assumed to be present:
- AFK Kick picks a role (active_role_id) instead of a custom check
  function.
- Chat Auto Reply, Protection, and Nuke Protection no longer take a
  permission as a setting.

New features, each chosen for an edge case:
- Discord Alerts: conditional handlers, and a command map that becomes
  a list.
- Entity Protection: lists turned into sets at load, and bookkeeping
  that must keep running while disabled.
- Nuke Protection: config keyed by defines, flattened to a banned items
  list.
- Popups: one config shared by two control files.
- Spawn Area: nested sections, and settings that only apply when the
  map is created.

Page: now laid out like the role page. It has a search across every
setting, one long list with a sticky sidebar to jump between sections,
and the enabled switch separated at the front of each header with
Reset to defaults on the right. Changed rows are highlighted, and a
bottom bar offers Revert and Apply. Role settings use a role picker and
lists use a tag select.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:28:23 +00:00
bbassieandClaude Opus 5.5 69e9f172a3 Cover guis, commands, and intervals in the features framework
Review feedback from Cooldude on the first version.

Features.register is renamed to Features.config, and guard also takes a
feature name so a feature without settings needs no config call.
Autofill uses this. Small configs now live at the top of the file that
uses them: death markers, mine depletion, station auto name, and
lawnmower. AFK kick and chat auto reply keep separate files because
they contain functions.

Periods no longer need a restart. A lib can list intervals, which map a
config key to a handler that runs every config[key] seconds. They are
checked once a second with a stateless tick check. AFK kick and death
markers use this and expose their periods on the page.

Guis: receive_update raises on_feature_changed, and control/features.lua
refreshes every player's gui when it fires. Autofill's toolbar button
checks Features.is_enabled, and hiding the button also hides its left
element. Quick actions refresh on the event too.

Commands: add_flags{ feature = ... } ties a command to a feature, and a
new authority denies it while the feature is disabled. The custom help
and quick actions already leave out denied commands. Lawnmower uses this
and is now added as a lib, so its destroy_decoratives handlers are
actually registered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 18:43:31 +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
Cooldude2606 5b451f160a Remove graftorio 2026-09-27 01:08:38 +01:00
bbassieandClaude Opus 5.5 9ddb7b362b Move the report commands into exp_reports
The create-report, get-reports and clear-reports commands now live in the
plugin's own module next to the functions they call, with their locale.
The permissions keep their exp_scenario.command names because the command
authority derives them from the command name.

The Discord report alert in exp_scenario is gone since exp_reports posts to
its own webhook. The shared add_player stub takes an options table with
connected and admin defaulting to true and false, replacing the copy in
the exp_reports test environment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 23:22:18 +00:00
Cooldude2606 e47f41d440 Merge branch 'main' into feature/reports-plugin 2026-09-26 23:27:12 +01:00
BastiaanandCooldude2606 862fe49b60 Move the virtual layer into exp_scenario (#463)
The virtual layer was a legacy control module and a legacy gui built on
the legacy event system. The control module moves to
exp_scenario/module/control/vlayer.lua in the event_handler shape, with the
data typed and the allowed item table built locally rather than by
rewriting the config. The gui moves to exp_scenario/module/gui/vlayer.lua
in the shape of the other scenario guis: statistics as label and bar
defines refreshed from display data computed once, and the interface
controls as a define which owns its dropdowns.

Api changes while moving: interfaces are removed by entity rather than by
searching a position, the dead circuit wiring parameter is gone since
connect_neighbour no longer exists, and remove_item now takes only the
unallocated amount out of storage and counts that production back out of
the total, which the legacy module got wrong.

The locale moves to [exp-gui_vlayer] in en, zh-CN and zh-TW and the legacy
[vlayer] sections, modules and file loader entries are removed.

Co-authored-by: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com>
2026-09-26 22:46:08 +01:00
bbassie 68b2dda190 Move the warp list into exp_scenario
The warp system was split across a legacy control module and a legacy gui,
both built on expcore.datastore. They are now one exp_scenario gui,
exp_scenario/module/gui/warp_list.lua, in the shape of the task list: per
force warps and per player cooldown and proximity state live on the
container's element data, rows are element defines which carry their warp in
element data, and refreshes compute the player's status once and apply it to
every row. Warps are referenced as objects rather than string ids, the last
editor is a LuaPlayer, and the cooldown counts ticks.

The locale moves to [exp-gui_warp-list] in en, zh-CN and zh-TW with keys
named by region and use, and the legacy [warp-list] sections are removed
along with the two legacy modules and the file loader entry. The config
stays in exp_legacy/config/gui/warps.lua for now like the other gui configs.
2026-09-26 22:12:21 +01:00
c4ca3d6738 chore(l10n): update Chinese (Traditional Han script) translation (#45) (#478)
chore(l10n): update Chinese (Simplified Han script) translation

chore(l10n): update Chinese (Traditional Han script) translation

chore(l10n): update Chinese (Simplified Han script) translation

chore(l10n): update Chinese (Traditional Han script) translation

chore(l10n): update Chinese (Simplified Han script) translation

chore(l10n): update English translation


Language: Chinese (Simplified Han script)
Language: Chinese (Traditional Han script)
Language: English
Progress: 100.0% (233 of 233 strings)
Progress: 100.0% (452 of 452 strings)
Progress: 95.7% (433 of 452 strings)
Translate-URL: https://translate.aperx.org/projects/expcluster/explegacy/zh_Hans/
Translate-URL: https://translate.aperx.org/projects/expcluster/explegacy/zh_Hant/
Translate-URL: https://translate.aperx.org/projects/expcluster/expscenario/en/
Translate-URL: https://translate.aperx.org/projects/expcluster/expscenario/zh_Hans/
Translate-URL: https://translate.aperx.org/projects/expcluster/expscenario/zh_Hant/
Translation: ExpCluster/ExpLegacy
Translation: ExpCluster/ExpScenario

Co-authored-by: aperx-translate[bot] <328484987+aperx-translate[bot]@users.noreply.github.com>
Co-authored-by: Weblate <noreply@weblate.org>
2026-09-26 21:35:30 +01:00
phidiasandWeblate 23ee68c50a Further locale change (#471)
* chore(l10n): update translation files

Add-on: Cleanup translation files
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.aperx.org/projects/expcluster/expscenario/
Translation: ExpCluster/ExpScenario

* Update readme.lua

* links

* readme

* chore(l10n): update English translation

chore(l10n): update Chinese (Traditional Han script) translation

chore(l10n): update Chinese (Simplified Han script) translation

chore(l10n): update Chinese (Traditional Han script) translation

chore(l10n): update Chinese (Simplified Han script) translation

chore(l10n): update Chinese (Traditional Han script) translation

chore(l10n): update Chinese (Simplified Han script) translation

chore(l10n): update Chinese (Traditional Han script) translation

chore(l10n): update Chinese (Simplified Han script) translation

chore(l10n): update English translation

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: phidias <phidias@aperx.org>
Language: Chinese (Simplified Han script)
Language: Chinese (Traditional Han script)
Language: English
Progress: 100.0% (1 of 1 strings)
Progress: 100.0% (2 of 2 strings)
Progress: 100.0% (233 of 233 strings)
Progress: 100.0% (4 of 4 strings)
Translate-URL: https://translate.aperx.org/projects/expcluster/explegacy/zh_Hans/
Translate-URL: https://translate.aperx.org/projects/expcluster/explegacy/zh_Hant/
Translate-URL: https://translate.aperx.org/projects/expcluster/exprole/en/
Translate-URL: https://translate.aperx.org/projects/expcluster/exprole/zh_Hans/
Translate-URL: https://translate.aperx.org/projects/expcluster/exprole/zh_Hant/
Translate-URL: https://translate.aperx.org/projects/expcluster/expserverups/zh_Hans/
Translate-URL: https://translate.aperx.org/projects/expcluster/expserverups/zh_Hant/
Translate-URL: https://translate.aperx.org/projects/expcluster/exputil/zh_Hans/
Translate-URL: https://translate.aperx.org/projects/expcluster/exputil/zh_Hant/
Translation: ExpCluster/ExpLegacy
Translation: ExpCluster/ExpRole
Translation: ExpCluster/ExpServerUPS
Translation: ExpCluster/ExpUtil

* chore(l10n): update Chinese (Traditional Han script) translation

chore(l10n): update Chinese (Simplified Han script) translation

chore(l10n): update English translation

Co-authored-by: phidias <phidias@aperx.org>
Language: Chinese (Simplified Han script)
Language: Chinese (Traditional Han script)
Language: English
Progress: 100.0% (452 of 452 strings)
Progress: 95.7% (433 of 452 strings)
Translate-URL: https://translate.aperx.org/projects/expcluster/expscenario/en/
Translate-URL: https://translate.aperx.org/projects/expcluster/expscenario/zh_Hans/
Translate-URL: https://translate.aperx.org/projects/expcluster/expscenario/zh_Hant/
Translation: ExpCluster/ExpScenario

* chore(l10n): update Chinese (Traditional Han script) translation

chore(l10n): update Chinese (Simplified Han script) translation

Co-authored-by: phidias <phidias@aperx.org>
Language: Chinese (Simplified Han script)
Language: Chinese (Traditional Han script)
Progress: 95.7% (433 of 452 strings)
Translate-URL: https://translate.aperx.org/projects/expcluster/expscenario/zh_Hans/
Translate-URL: https://translate.aperx.org/projects/expcluster/expscenario/zh_Hant/
Translation: ExpCluster/ExpScenario

---------

Co-authored-by: Weblate <noreply@weblate.org>
2026-09-09 18:43:29 +01: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
bbassie bad98c5013 Add the exp_reports plugin and move the scenario onto it
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.
2026-09-06 16:44:23 +00:00
phidias 142d296a02 Merge pull request #455 from PHIDIAS0303/patch-1
Localization
2026-09-06 17:05:04 +01:00
Cooldude2606 e14e27c158 Merge pull request #466 from bbassie/fix/quickbar-pages
Use page and slot for quick bar filters
2026-09-06 01:08:32 +01:00
Cooldude2606 01bc4cd14d Added constant for total page count 2026-09-06 01:07:19 +01:00
Cooldude2606 09fe5d5f8d Merge pull request #464 from bbassie/fix/vehicle-kills
Count kills and damage dealt from vehicles
2026-09-06 00:42:54 +01:00
Cooldude2606 98545f1828 Merge branch 'main' into feature/remove-warnings 2026-09-06 00:31:28 +01:00
Cooldude2606 42f177a58e Merge pull request #460 from bbassie/feature/scenario-spectate
Move spectate into exp_scenario
2026-09-06 00:27:52 +01:00
Cooldude2606 ac7cdbe01a Merge branch 'main' into feature/scenario-protection 2026-09-06 00:13:41 +01:00
bbassie f961a8d82e Use page and slot for quick bar filters
Factorio 2.1 changed get_quick_bar_slot and set_quick_bar_slot to take a
page and slot rather than a single index, and to describe slots holding
records, remotes and specific item instances. Saved filters keep their
single index, which is converted on load and save, and only plain item
filters are saved since the other slot types hold data which has no name
to store. The command is enabled again.

Fixes #444
2026-09-05 20:54:21 +00:00
bbassie 37eff4b3e9 Count kills and damage dealt from vehicles
The statistics only credited damage and kills when the cause was a
character, so anything done from a car, tank, spidertron or locomotive was
lost. The cause is now resolved to a player through the vehicle's driver,
or passenger when there is no driver, and turrets, remotes and uncrewed
trains still count for nobody.

Fixes #219
2026-09-05 20:53:56 +00:00
bbassie a59b2a8414 Remove the warning system
Warnings were a moderator tool layered on top of reports and had no
callers left outside their own command and the player list button. The
legacy module, its config and locale, the exp_scenario command and its
permissions, the discord alert block and the player list button are all
removed. Reporting from the player list no longer depends on whether the
player could give warnings, anyone can report a player who is not immune.
2026-09-05 19:22:37 +00:00
bbassie 8014cb1e77 Move spectate into exp_scenario
The spectate and follow module moves to exp_scenario/module/control/spectate.lua
in the event_handler shape the other control modules use, and the copy in
exp_legacy is removed. Follow state is a typed record rather than a
positional table, the follow label caption is localised, and the escape
handler marks the record to stop on the next tick instead of poking an
invalid position into it.

The LuaPlayer.close_map call is gone: it does not exist in Factorio 2.0,
where the map is the remote controller and set_controller replaces it.
2026-09-05 19:20:55 +00:00
bbassie 0c52532c05 Move protection into exp_scenario
The entity protection module moves to exp_scenario/module/control/protection.lua
in the event_handler shape the other control modules use, and the copy in
exp_legacy is removed. The config lookups are built as local sets instead of
rewriting the config tables in place, the two mined handlers become one, and
the entity and area key helpers are exposed so the protection command no
longer keeps its own copies. Event ids are exposed as
Protection.on_player_mined_protected and Protection.on_repeat_violation.
2026-09-05 19:19:45 +00:00
bbassie 2e53e4668d Move jail into exp_scenario
With the role system finished the jail module has nothing legacy left in
it, so it moves to exp_scenario/module/control/jail.lua and the copy in
exp_legacy is removed. The module takes LuaPlayer only, requires a reason,
returns false rather than nil when there is nothing to do, and exposes its
event ids as Jail.on_player_jailed and Jail.on_player_unjailed in the same
shape as exp_util's selection events.
2026-09-05 19:18:04 +00:00
bbassieandClaude Fable 5 bea8d46c82 Address review on the role api
- Roles are objects and everything done to or with a role is a method on it:
  assign, unassign, has_player, has_permission, is_higher_than,
  is_lower_than, get_players, get_player_names, print. Assignment has one
  entry point, role:assign(player, options), with local_only as an option
  rather than a second function.
- Roles are looked up by clusterio id with get_role; get_role_by_name searches
  the list for the few places, such as configs, which only know a name. The
  name map and the ordered list are gone, get_roles sorts on demand and the
  index field is replaced by the comparison methods.
- Players are LuaPlayer objects only, with nil or index 0 for the server.
- get_higher_roles and get_lower_roles replace print_to_roles_higher and
  print_to_roles_lower, call sites loop over them with role:print.
- Permission groups are removed from roles again, exp_groups owns the mapping
  from roles to groups.
- Seeding is a SeedRolesRequest behind a button on the roles page rather than
  running on first start, and creates only the roles; the player assignments
  are dropped. The seed lists each permission once at the lowest role which
  has it and lets the parent chain carry it up.
- System commands unlock for core.admin rather than a permission of their own.
- Role metatables are registered with Storage.register_metatable so the
  methods survive save and load, which the role records in storage needed.
- The player list auth uses Roles.player_outranks directly, and the event
  carries role ids rather than names.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 15:52:50 +00:00
bbassieandClaude Fable 5 7034f17b5d Move the scenario onto exp_roles and remove the legacy role system
Every call site of expcore.roles now uses exp_roles, and the legacy module,
its config, and the glue which refreshed guis on role events are deleted.
Where a file only renamed the require and the permission strings the change is
mechanical; the rest:

- Jail is now "give the Jail role" and unjail "take it away". The role has a
  higher priority than every other so holding it suppresses them, which is
  what stashing and restoring the roles was for.
- The command role authority derives exp_scenario.command.<name> from the
  command name, and the role parsers use player_outranks rather than comparing
  indexes with their own root check.
- The admin and spectator triggers, and the gui refresh on role changes, live
  in exp_scenario/control/roles.lua; the system commands trigger stays with
  the command authority.
- The player list warn button is keyed on create_warning, the permission
  the command behind it already required, and report on create_report. Both
  were keyed on names no role held, so only root ever saw them.
- The warps and tasks configs say exp_roles where they said expcore.roles.
- The role tables the readme and player list read are replaced by
  get_player_names and get_roles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 11:36:58 +00:00
Cooldude2606 5890155331 Final manual fixups 2026-08-08 23:09:39 +01:00
Cooldude2606 1e861c5c0b Fix issue with MapPosition.energy 2026-08-08 21:58:46 +01:00
bbassieandClaude Opus 5 619ed4b39f Remove asserts the types already cover
The nil check burn down added guards which turned out redundant once
the surrounding annotations landed. `_has_handlers` is set to true when
the first handler registers, so it is a boolean rather than the literal
false it was inferred as.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:48 +00:00
bbassieandClaude Opus 5 f907007d14 Fix arity and return value mismatches
`assert` returns every argument it is given, so `return assert(x, msg)`
was leaking the message as a second return value.

`get_tile` is documented as taking x and y. The async function class
declared `@operator call` with no parameters, which made every async
call look over supplied.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:48 +00:00
bbassieandClaude Opus 5 f5656025ee Clear the last of the nil checks
Guards now cover the statements that follow them, and a couple of reads
are restructured so the narrowing survives.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:43 +00:00
bbassieandClaude Opus 5 76ecc37d3f Type the legacy role registry
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:43 +00:00
bbassieandClaude Opus 5 b5bbf73332 Patch MapPosition and BoundingBox to the named form
The api documents both as a union with a positional array because both
are accepted as input, which makes every read of `.x` or `.left_top`
optional. Everything read back from the game uses the named form, so
the positional variant is removed and we now always write it that way
too.

The spawn area config offsets and the mine depletion search areas were
the only positional writes left, and apply_offset no longer needs its
fallback.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:43 +00:00
bbassieandClaude Opus 5 f9ce3c4145 Assert row lookups and narrow force and style reads
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:43 +00:00
bbassieandClaude Opus 5 0b31a70801 Type forward declared handlers and assert element parents
The player list action setter runs during control setup, before any of
the callbacks that use it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:43 +00:00
bbassieandClaude Opus 5 aca8bc79ae Assert the vlayer gui lookups
Dropdowns are siblings of the button that reads them, and the loop
bounds come from the interface count.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:43 +00:00
bbassieandClaude Opus 5 d44381bac1 Assert the vlayer gui lookups
Dropdowns are siblings of the button that reads them, and the loop
bounds come from the interface count.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:43 +00:00
bbassieandClaude Opus 5 d25fc42538 Assert vlayer runtime invariants and type its storage
The interfaces are chests and constant combinators, so the inventory,
logistic sections and first circuit section always exist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:43 +00:00
bbassieandClaude Opus 5 7bb874f73f Describe the legacy role type
get_player_roles always returns at least the default or root role, so
get_player_highest_role never returns nil, and callers were right to
treat it that way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:31:43 +00:00