Move the scenario onto exp_roles and remove the legacy role system - #453
Move the scenario onto exp_roles and remove the legacy role system#453bbassie wants to merge 6 commits into
Conversation
The module no longer presents the interface of the legacy expcore.roles module. Nothing outside this repository depends on it, so rather than carry the legacy action strings and the transform which mapped them onto permission names, call sites now check the clusterio permission name directly. That removes the one invariant which silently broke every check if the lua and typescript transforms drifted, and makes a check in lua greppable against its definition. - player_allowed and player_has_flag become player_has_permission; flags were only permissions with a change trigger, which define_permission_trigger now provides for any permission. - on_role_assigned and on_role_unassigned become one on_player_roles_changed event carrying the assigned and unassigned names. Every consumer registered both for the same handler. It is also raised for connected players when a role is edited on the controller, which the old events never were, and for changes made on the controller to the roles a player holds. - player_outranks and player_outranks_role replace the repeated comparison of highest role indexes, and apply the core.admin bypass consistently, which two of the six call sites did not. - get_role takes a name, clusterio id, or role; get_roles replaces get_roles_ordered. The config views of the roles are gone, with role:get_player_names covering the one use of config.players. - Roles carry a permission group, which the legacy system mapped roles to and the first version of the plugin dropped. A player is moved into the group of their most privileged role which names one. It is edited with the other in game properties. - skip_checks is dropped from assign_player and unassign_player. A player object with index 0 is treated as the server, which is how exp_commands represents rcon. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With the lua side checking permission names directly there is no transform to derive them from, so each permission is listed with its name. This was also the chance to drop the legacy action and flag buckets, which only reflected how the old config was written: - exp_scenario.bypass.* for entity protection, nuke protection, the deconstruction log, and reports. - exp_scenario.decon.* for the two deconstruction levels, with descriptions which say what they gate. - exp_scenario.player.* for admin, spectator, instant respawn, and system commands. - exp_scenario.chat.commands, and exp_scenario.gui.player_list.kick and .ban for the player list buttons which were never commands. Commands derive their permission as exp_scenario.command.<name>, so assign-role, unassign-role, and get-roles get scenario permissions rather than the core ones they mapped to before. The in game command is bounded by the lower role check, while core.user.update_roles is not, so granting it to moderators would have let them change any role from the web ui. Dropped: defer_role_changes, which priority replaced; command/give-warning, which no role held and the player list now checks create_warning for; and command/report, which was never defined. clear-tag/always is renamed to tag_clear.always to match the command it belongs to. _ipc and _sudo are added so every command has a definition. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
When the role properties datastore is empty the plugin has not run before, so the roles the scenario used to define are created on the controller, along with the players the config listed. This replaces the role config which was loaded into every map. The seed keeps the parent relationships of the old config and flattens them into the permissions of each role, since clusterio roles do not inherit. The default and admin roles already exist, so those entries only set the in game properties; the default role gets its permissions through grantByDefault. Roles which already exist by name are reused and only gain the seed permissions, so seeding an existing cluster is safe. Permissions which are not defined are logged rather than refused, in case a plugin is not loaded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cooldude2606
left a comment
There was a problem hiding this comment.
Along with all the changes highlighted in this review; can you also weigh the pros and cons of using oop for the roles vs only using module level methods. At the moment there are a few places where we have mutliple ways to do things or where things can be cumbersome, so we should align on a strong dx api interface for working with roles and covering the use cases we have or would expect.
| permission = "exp_scenario." .. body | ||
| --- Move a player into a permission group, done async because the game does not | ||
| --- allow it from within every event | ||
| local set_permission_group_async = Async.register(function(player, group) |
There was a problem hiding this comment.
Roles should not interact with permission groups anymore. This is the responsibility of exp_groups. Therefore this can be removed.
| --- Roles in order, the most privileged first, rebuilt from script data | ||
| local ordered_roles = {} --- @type ExpRoles.Role[] | ||
| --- Roles indexed by name, rebuilt from script data | ||
| local roles_by_name = {} --- @type table<string, ExpRoles.Role> |
There was a problem hiding this comment.
We should have scripts use the role id not the name. So we dont need this mapping. When we need to get a role by name (which will be rare) we can search the list of roles.
| Permission names | ||
| ]] | ||
| --- Roles in order, the most privileged first, rebuilt from script data | ||
| local ordered_roles = {} --- @type ExpRoles.Role[] |
There was a problem hiding this comment.
Why do we need to maintain this order? The only time we check index is when doing direct comparisons, so we dont need this list.
| custom_active_check = function(player) | ||
| return Roles.get_player_highest_role(player).index <= Roles.get_role_from_any("Veteran").index | ||
| local veteran = Roles.get_role("Veteran") | ||
| return veteran ~= nil and Roles.get_player_highest_role(player).index <= veteran.index |
There was a problem hiding this comment.
We should keep "index" internal and instead expose a method for doing comparisons.
| --- Check if a player is more privileged than a role | ||
| --- A player with core.admin, which includes the server, outranks every role | ||
| --- @param player LuaPlayer | string | nil | ||
| --- @param role string | number | ExpRoles.Role |
There was a problem hiding this comment.
It is bad practice to have mutliple ways to specify a role. We should only use role objects only, and have them be methods instead.
| if player_highest.index < action_player_highest.index then | ||
| return true | ||
| end | ||
| return Roles.player_outranks(player, selected_player_name) |
There was a problem hiding this comment.
We can use this directory rather than as a proxy local function.
| shortHand: string; | ||
| tag: string; | ||
| color: string | { toRgb(): { r: number, g: number, b: number } } | null; | ||
| permissionGroup: string; |
There was a problem hiding this comment.
As previous comment. We should not have permission groups as part of roles.
|
|
||
| // An empty datastore means the plugin has not run before, so the roles | ||
| // the scenario used to define are created on the controller | ||
| if (this.roleMeta.size === 0) { |
There was a problem hiding this comment.
This should be a manual button trigger and not automatic. Only the roles and permissions should be seeded, not the player assignments.
| permissions: string[]; | ||
| } | ||
|
|
||
| const admin = ["exp_scenario.player.admin", "exp_scenario.player.spectator", "exp_scenario.bypass.reports"]; |
There was a problem hiding this comment.
Can we inline these at the lowest place in the hierarchy. Including these individually is unneeded repition.
| end) | ||
|
|
||
| Roles.define_flag_trigger("is_system", function(player, state) | ||
| Roles.define_permission_trigger("exp_scenario.player.system_commands", function(player, state) |
There was a problem hiding this comment.
We can change this to be based on core.admin
- 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>
|
Pushed bea8d46 covering every point. The api question first, since it decided the shape of the rest. Object methods vs module functionsI weighed it as: module functions are easiest when the role is incidental to the call — a permission check where the caller has a player and a permission name and never thinks about roles, or anything that spans roles such as ordering. Methods are easiest when the caller already holds a role, because there is only one way to say it and the role cannot be given three ways (name, id, object) as it could before. The old api mixed both, which is where the duplication came from: So the split is now by what the caller has in hand:
There is one assignment entry point, The one cost of methods worth naming: a call site that wants the roles of a player and then something about each of them now does two steps, The rest
Tests updated for the new api, 108 assertions passing; emmylua still 0 findings; tsc and the web bundle clean; the instance starts with the full scenario and the methods answer as expected over rcon. |
Part two of #448. Moves every call site off
expcore.roles, deletes the legacy module and config, simplifies jail, and seeds the roles on first run. Taking "improve the role api with no restraint, no external consumers other than the mono repo" at its word, the lua api is redesigned rather than kept compatible, and the permission names are tidied up while nothing holds them yet.Lua api
The legacy action strings and the transform which mapped them onto permission names are gone. Call sites check the clusterio permission directly, so
grep exp_scenario.command.killfinds the definition, the check, and the seed, and there is no second transform which silently breaks every check if it drifts.player_allowed(p, "command/kill")player_has_permission(p, "exp_scenario.command.kill")player_has_flag(p, "report-immune")player_has_permission(p, "exp_scenario.bypass.reports")define_flag_trigger(flag, fn)define_permission_trigger(permission, fn)— any permission can have oneevents.on_role_assigned/on_role_unassignedevents.on_player_roles_changedwithassignedandunassignednamesget_player_highest_role(a).index < get_player_highest_role(b).indexplayer_outranks(a, b), plusplayer_outranks_role(a, role)get_role_from_any/get_role_by_name/get_roles_orderedget_role(name | id | role)/get_roles()config.order/config.roles/config.playersrole:get_player_names()covers the one useassign_player(p, roles, by, skip_checks, silent)assign_player(p, roles, by, silent)Flags were only permissions with a change trigger, so the separate concept goes. Every consumer registered both role events for the same handler, so they become one, and it is now also raised when the roles a player holds change on the controller, or when a role they hold is edited there. The old events were never raised for either, which left guis stale after a web ui change.
player_outranksapplies the core.admin bypass consistently; two of the six index comparisons did not.A role now carries a permission group, which the legacy system mapped roles to and #448 dropped. A player is moved into the group of their most privileged role which names one, which is how Jail actually restricts a player. It sits with the other in game properties on the role page.
Permission names
Explicit list in
exp_scenario/permissions.ts, no transform. Theactionandflagbuckets only reflected how the old config was written, so:exp_scenario.bypass.*(entity protection, nuke protection, deconstruction log, reports),exp_scenario.decon.*(standard, fast trees — with descriptions that say what they gate),exp_scenario.player.*(admin, spectator, instant respawn, system commands),exp_scenario.chat.commands, andexp_scenario.gui.player_list.kick/.banfor the player list buttons which were never commands.command.*andgui.*are unchanged apart fromclear_tag.always→tag_clear.alwaysto match the command it belongs to.Commands derive
exp_scenario.command.<name>in the authority, soassign-role,unassign-roleandget-rolesget scenario permissions rather than the core ones #448 mapped them to. The in game command is bounded by the lower role check,core.user.update_rolesis not — granting it to Moderator would have let any mod with a web account change anyone's roles.Dropped:
defer_role_changes(priority replaced it),command/give-warning(no role held it; the player list now keys the warn button oncreate_warning, the permission the command already needed) andcommand/report(never defined; nowcreate_report). Both buttons were only ever visible to root._ipcand_sudoare added so every command has a definition.Jail
jail_playergives the Jail role,unjail_playertakes it away.Jail.old_rolesand the stash/restore are gone, since priority suppresses the other roles for as long as the role is held.Seeding
On the first run (empty role properties datastore) the controller creates the roles the scenario shipped with and gives the listed players their roles. The seed keeps the
parentrelationships from the old config and flattens them, since clusterio roles do not inherit; the flattened sets were diffed against the legacy config and match for every role. Cluster Admin and the default role already exist, so those entries only set properties — the default role gets its permissions throughgrantByDefault, which is checked to equal the old Guest list. Roles which already exist by name are reused and only gain permissions, so seeding a cluster which already has some is safe.Verification
ctl user set-rolesreaching the game.Notes
exp_scenario.command.kill.alwaysandspawn.alwaysstay defined;/killnever checkedkill.alwaysbefore either, its lower role parser already bounds it. Left for a later tidy.controller run --devweb build is not set up here, the form only gains one text field next to the existing ones.🤖 Generated with Claude Code