exp_scenario imports types from @expcluster/roles and
@expcluster/permission-groups through their dist folders, so building it
on its own before those packages are built fails with TS2307. A plain
pnpm install runs the prepare scripts in dependency order, but a filtered
install or running tsc --build in exp_scenario on a fresh clone does not.
Referencing the node projects of both packages makes tsc build them first.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.
Clearing the slots on leave meant nothing was known about a player's ammo
until it changed, so the first shot after rejoining was missed. The slots
are now read when a player joins and when they respawn, since a new
character starts empty and a stale single round would otherwise count as
a shot. The change handler reads the slots the same way and compares them
with what was stored.
The cursor was a line of text under the camera, which read as an
afterthought next to the framed header. It is now a slot button in the
header after the zoom buttons: the item sprite with its count, greyed out
for a ghost, an empty slot for an empty cursor, and the item name in the
tooltip. The window keeps its shape whatever the item is called.
A label under the camera shows what the player being followed holds in
their cursor, item and count or the ghost item, so that using a
deconstruction planner or similar can be seen. It is hidden while the
camera shows a fixed location.
Closes#427
Factorio has no fired event, so the log was written on every ammo
inventory change, which includes loading rockets for the first time and
swapping between rocket types. The last seen name and count of each ammo
slot is now kept per player, and a shot is only logged when a slot holds
the same ammo with one fewer, or empties from a single round. Only the
three configured ammo types are logged, rather than every ammo change.
Fixes#242
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.
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.
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.
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.
- player_has_any_permission and player_has_all_permission, matching the
account checks the web ui has.
- Roles are no longer sorted on the way out of every lookup. get_ordered_roles
and the public sort_roles cover the two guis and the command which present
roles in order, and the highest role is found with a single scan.
- get_held_role_ids returns the list and the set it already built, rather than
a second function rebuilding the set from the list.
- get_player_names collects into a set before listing, so a player holding the
role in both the synced and the local list is counted once.
- The role metatable is registered directly under the plugin name, dropping
the storage import.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 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>
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>
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>
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>
`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>
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>
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>
spawn_area accepts both position forms on purpose, so it asserts that
one of the two is present rather than narrowing the type.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Color.struct leaves every channel optional, so the rainbow command
declares its own fully populated colour.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rows and selectors are created alongside the machine selector, so they
exist for anything already in the table.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A union of table types makes every lookup optional. Separate index
signatures on one class say the same thing without the nil.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Values read back from the game always use the named members, so they
are narrowed once where they enter a function rather than at each use.
`LuaControl.force` is a ForceID union, so reading a force only method
off it needs the cast the rest of the repo already uses.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Roles.new_role` returned the result of `error`, which made it nullable,
and `set_permission_group` returned nil for an unknown group, which
broke the config chain with an index error rather than saying so.
Positions and bounding boxes read back from the game always use the
named members, so they are narrowed once where they enter a function.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Locals declared ahead of an assignment inside a callback were inferred
as nil at every use site.
The role event handlers are given a class so the table is not unified
with the other module handler tables.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`loadstring` was removed in Lua 5.2, `load` takes a string the same way.
Warp gui child lookups are optional, so they use the same assert the
rest of the repo does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`LogisticFilter.signal` became `value` in 2.0, so the clear loop broke
on the first slot and outdated signals were left behind.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Autofill stores the whole per entity map against a player.
The readme container is called as a define, so it needs the overload
the other element classes carry.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`_display_data` is a cache keyed by force name, not a single record.
The auto complete accumulator starts empty, so its fields are optional.
Game commands have no usage beyond their own help text.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`config.temp_warning_limit` does not exist, the warnings config defines
`script_warning_limit`, and the locale expects it as `__3__/__4__`, so
the command was printing nil.
map_tags is keyed by the force name joined to the tag number, and
custom_color is the Color union.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
emmylua only parses the inline cast as `--[[@as T]]`, the spaced form
`--[[ @as T ]]` is treated as a plain comment, so all 149 of them were
doing nothing.
LuaGuiElement.style is a union because a style name can be assigned to
it, so reading it back needs the cast.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`data` on an element is the GuiData store keyed by element, not the
value being stored.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The prune loop shadowed its own parameter, so `silo_data[unit_number] =
nil` wrote into the entry instead of removing it from the table.
`unit_number` is optional on LuaEntity but always set for a silo. Note
that `--[[ @as ]]` does not narrow for an index expression, only
`@cast` does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>