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>
- The instance sends every value of a feature, defaults included, so
update_config is one loop which sets each field and compares. The
stored table is the last set of values, and _apply is gone.
- values_equal compares table sizes before the contents.
- Protected entities are registered with register_on_object_destroyed
and forgotten in on_object_destroyed, which covers every way an
entity can go. The four removal handlers and the validity checks are
gone. Entities destroyed while the feature is disabled are swept when
it is enabled again. Saves from before the registrations table get it
through the Storage on_init callback.
- Spawn area applied the water offset twice and the pattern offset
never. The pattern tiles now move with the entities above them.
- pruneFeatureValues reads a validation result instead of catching an
error, and FeaturesPage hands its pending state and unsaved bar to
helpers.
- Shorter comments throughout.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Feature.register returns the config as a second value, so files
write `local feature, config = Feature.register(...)`.
- The lib is ExpScenario.Features and the instance class with its
fields sits on Feature._prototype, which is the __index, the same
shape as exp_commands.
- update_config only looks at values whose override was added,
removed, or replaced. _apply_field sets one value and returns it, so
the comparison happens while applying and the second loop only
raises. Overrides dropped from the previous set count as candidates
because the value reverts to its default.
- The `@setting name` tags are gone from the config comments.
- Protection is guarded as a whole instead of checking config.enabled
in one handler. The handlers also forget mined entities, so
get_entities and is_entity_protected now drop invalid references,
which also covers entities destroyed without an event. The list
getters are gone, the selection overlay builds its lists from
Protection.config.
- Chat and damage popups are one feature in control/popups.lua.
- ignore_admins and command_admin_only are removed, roles cover them.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- The page waited for nothing, so before the subscription delivered
the records every feature looked enabled with its defaults, and an
Apply in that window would have written that state. It now shows a
spinner until synced, like the role page.
- Values stored for a setting which was since renamed, removed, or
changed type made every later save of that feature fail validation.
The controller now reconciles the stored records with features.ts on
start: stale values are dropped with a warning, records of removed
features are deleted, and new features get their defaults.
- update_config no longer stores overrides for unknown paths, so a
stale key is warned about once instead of on every load.
- The armor and gun inventory handlers of nuke protection are new to
this branch and asserted on get_inventory, which is nil without a
character. They return instead.
- The AFK check ran every 30 minutes while afk_minutes and kick_minutes
can be set as low as 1. It runs every minute.
- sameValue lives in features.ts and isDefaultValue uses it, and the
custom input branch of FieldInput is a single narrowed check.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Feature.to_set made every feature turn its config lists into sets on
each lookup, which is work the framework should do once. Sets are now a
field type of their own. Feature.set{...} declares one, the controller
sends a list, and _apply converts it when the override is applied, on
load and on update alike. Features index the config directly, and
to_set and its cache are gone.
Lists are explicit too. Feature.list{...} declares one, where before
any table of strings, and any empty table, counted as a list. Untagged
tables with string keys are nested sections, and other tables can not
be overridden. Feature.optional uses the same marker as list and set.
On the web side a string list can no longer name an input component,
so the cast that made one look like an object field is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review points from Cooldude:
- Feature.register(name[, config]) is required again and returns a
feature object. Each feature has its config, its fields, and its
overrides, so one registry holds all the data. Only the overrides are
stored. On load each feature points at its own stored table again.
- The methods are feature:guard(lib), feature:update_config(enabled,
values), and feature:is_enabled(). Feature.get(name) looks up a
feature, and commands flag the feature object.
- update_config raises on_config_updated once per changed value, with
the feature name, path, old value, and new value. The gui refresh only
reacts to the "enabled" path.
- apply only sets values. on_apply is gone. Lookup sets built from
lists use Feature.to_set, which is cached by the list itself. A
replaced list gets a new set, so the sets are also correct after a
load. Protection exposes getters for its protected names and types.
Web: settings use small inputs, the same size as the config pages.
Fields are shaped like clusterio config field definitions. A field
with an inputComponent renders the component registered with the web
interface, so the role setting now uses core's role input, and other
plugin inputs can be used later.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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.
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.
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.
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>
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>