mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2026-11-11 15:54:52 +09:00
Rework features into a Feature class with per field events
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>
This commit is contained in:
1 parent
4bc1773c82
commit
628e4df514
26 files changed
+468
-430
No files matched your search
+27
-10
@@ -8,17 +8,24 @@
|
||||
|
||||
export type FeatureValue = boolean | number | string | string[] | null;
|
||||
|
||||
/**
|
||||
* A setting of a feature, shaped like a clusterio config field definition so
|
||||
* the web interface can render it with the same inputs, including the input
|
||||
* components registered by plugins.
|
||||
*/
|
||||
export type FeatureField = {
|
||||
name: string,
|
||||
title: string,
|
||||
description: string,
|
||||
/** Allows null, the lua side uses Feature.optional */
|
||||
optional?: boolean,
|
||||
/** Name of an input component registered with the web interface, such as "role" */
|
||||
inputComponent?: string,
|
||||
} & (
|
||||
| { type: "boolean", default: boolean }
|
||||
| { type: "number", default: number, min?: number, unit?: string }
|
||||
| { type: "string", default: string }
|
||||
| { type: "number", default: number | null, min?: number, unit?: string }
|
||||
| { type: "string", default: string | null, enum?: string[] }
|
||||
| { type: "string_list", default: string[] }
|
||||
/** Id of a clusterio role, null when no role is picked */
|
||||
| { type: "role", default: null }
|
||||
);
|
||||
|
||||
export type Feature = {
|
||||
@@ -44,8 +51,9 @@ function list(name: string, title: string, description: string, value: string[])
|
||||
return { name, title, description, type: "string_list", default: value };
|
||||
}
|
||||
|
||||
/** Id of a clusterio role, null when no role is picked */
|
||||
function role(name: string, title: string, description: string): FeatureField {
|
||||
return { name, title, description, type: "role", default: null };
|
||||
return { name, title, description, type: "number", default: null, optional: true, inputComponent: "role" };
|
||||
}
|
||||
|
||||
export const features: Feature[] = [
|
||||
@@ -229,17 +237,18 @@ export function validateFeatureValues(name: string, values: Record<string, Featu
|
||||
}
|
||||
|
||||
function checkFieldValue(field: FeatureField, value: FeatureValue, label: string) {
|
||||
if (value === null) {
|
||||
if (!field.optional) {
|
||||
throw new Error(`${label} can not be null`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
switch (field.type) {
|
||||
case "string_list":
|
||||
if (!Array.isArray(value) || value.some(item => typeof item !== "string")) {
|
||||
throw new Error(`${label} must be a list of strings`);
|
||||
}
|
||||
return;
|
||||
case "role":
|
||||
if (value !== null && !Number.isInteger(value)) {
|
||||
throw new Error(`${label} must be a role id or null`);
|
||||
}
|
||||
return;
|
||||
case "number":
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) {
|
||||
throw new Error(`${label} must be a finite number`);
|
||||
@@ -248,6 +257,14 @@ function checkFieldValue(field: FeatureField, value: FeatureValue, label: string
|
||||
throw new Error(`${label} must be at least ${field.min}`);
|
||||
}
|
||||
return;
|
||||
case "string":
|
||||
if (typeof value !== "string") {
|
||||
throw new Error(`${label} must be a string`);
|
||||
}
|
||||
if (field.enum && !field.enum.includes(value)) {
|
||||
throw new Error(`${label} must be one of ${field.enum.join(", ")}`);
|
||||
}
|
||||
return;
|
||||
default:
|
||||
if (typeof value !== field.type) {
|
||||
throw new Error(`${label} must be a ${field.type}`);
|
||||
|
||||
@@ -5,7 +5,7 @@ Adds a permission authority which checks the clusterio permission for a command
|
||||
local Commands = require("modules/exp_commands")
|
||||
local add, allow, deny = Commands.add_permission_authority, Commands.status.success, Commands.status.unauthorised
|
||||
|
||||
local Features = require("modules/exp_scenario/features")
|
||||
local Feature = require("modules/exp_scenario/features")
|
||||
local Roles = require("modules/exp_roles")
|
||||
local player_has_permission = Roles.player_has_permission
|
||||
|
||||
@@ -27,7 +27,7 @@ authorities.exp_permission =
|
||||
authorities.feature_enabled =
|
||||
add(function(player, command)
|
||||
local feature = command.flags.feature
|
||||
if feature and not Features.is_enabled(feature) then
|
||||
if feature and not feature:is_enabled() then
|
||||
return deny{ "exp-commands-authorities_feature.deny" }
|
||||
else
|
||||
return allow()
|
||||
|
||||
@@ -4,13 +4,14 @@ Adds a command that clean up biter corpse and nuclear hole
|
||||
|
||||
local AABB = require("modules/exp_util/aabb")
|
||||
local Commands = require("modules/exp_commands")
|
||||
local Features = require("modules/exp_scenario/features")
|
||||
local Feature = require("modules/exp_scenario/features")
|
||||
local Selection = require("modules/exp_util/selection")
|
||||
local SelectArea = Selection.connect("ExpCommand_Lawnmower")
|
||||
|
||||
local config = Features.config("lawnmower", {
|
||||
local feature = Feature.register("lawnmower", {
|
||||
destroy_decoratives = false, --- @setting destroy_decoratives remove decoratives under entities when they are built
|
||||
})
|
||||
local config = feature.config
|
||||
|
||||
--- @class ExpCommand_Lawnmower.commands
|
||||
local commands = {}
|
||||
@@ -19,7 +20,7 @@ local commands = {}
|
||||
--- @class ExpCommands_Lawnmower.commands.lawnmower: ExpCommand
|
||||
--- @overload fun(player: LuaPlayer)
|
||||
commands.lawnmower = Commands.new("lawnmower", { "exp-commands_lawnmower.description" })
|
||||
:add_flags{ feature = config }
|
||||
:add_flags{ feature = feature }
|
||||
:register(function(player)
|
||||
if SelectArea:stop(player) then
|
||||
return Commands.status.success{ "exp_util.selection_exit", { "exp-commands_lawnmower.selection-name" } }
|
||||
@@ -65,7 +66,7 @@ end
|
||||
|
||||
local e = defines.events
|
||||
|
||||
return Features.guard(config, {
|
||||
return feature:guard{
|
||||
events = {
|
||||
[e.on_built_entity] = destroy_decoratives,
|
||||
[e.on_robot_built_entity] = destroy_decoratives,
|
||||
@@ -73,4 +74,4 @@ return Features.guard(config, {
|
||||
[e.script_raised_revive] = destroy_decoratives,
|
||||
},
|
||||
commands = commands,
|
||||
})
|
||||
}
|
||||
@@ -166,15 +166,17 @@ local function on_player_selection_start(event)
|
||||
end
|
||||
|
||||
-- Show always protected entities by name
|
||||
if #EntityProtection.protected_entity_names > 0 then
|
||||
for _, entity in pairs(surface.find_entities_filtered{ name = EntityProtection.protected_entity_names, force = player.force }) do
|
||||
local protected_entity_names = EntityProtection.get_protected_entity_names()
|
||||
if #protected_entity_names > 0 then
|
||||
for _, entity in pairs(surface.find_entities_filtered{ name = protected_entity_names, force = player.force }) do
|
||||
show_protected_entity(player, entity)
|
||||
end
|
||||
end
|
||||
|
||||
-- Show always protected entities by type
|
||||
if #EntityProtection.protected_entity_types > 0 then
|
||||
for _, entity in pairs(surface.find_entities_filtered{ type = EntityProtection.protected_entity_types, force = player.force }) do
|
||||
local protected_entity_types = EntityProtection.get_protected_entity_types()
|
||||
if #protected_entity_types > 0 then
|
||||
for _, entity in pairs(surface.find_entities_filtered{ type = protected_entity_types, force = player.force }) do
|
||||
show_protected_entity(player, entity)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
-- @config Chat-Auto-Reply
|
||||
|
||||
local ExpUtil = require("modules/exp_util")
|
||||
local Features = require("modules/exp_scenario/features")
|
||||
local Feature = require("modules/exp_scenario/features")
|
||||
local Async = require("modules/exp_util/async")
|
||||
local config_server_detail = require("modules.exp_legacy.config.server_detail") --- @dep config.server_detail
|
||||
|
||||
@@ -25,7 +25,7 @@ local afk_time_units = {
|
||||
seconds = true,
|
||||
}
|
||||
|
||||
return Features.config("chat_auto_reply", {
|
||||
return Feature.register("chat_auto_reply", {
|
||||
--- @type table<string, LocalisedString | fun(player: LuaPlayer, is_command: boolean): LocalisedString>
|
||||
messages = { --- @setting messages will trigger when ever the word is said
|
||||
["discord"] = { "info.discord", config_server_detail["discord"] },
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
--- Popups for chat and damage, shared by control/chat_popup.lua and control/damage_popups.lua
|
||||
-- @config Popups
|
||||
|
||||
local Features = require("modules/exp_scenario/features")
|
||||
local Feature = require("modules/exp_scenario/features")
|
||||
|
||||
return Features.config("popups", {
|
||||
return Feature.register("popups", {
|
||||
show_player_messages = true, --- @setting show_player_messages weather a message in chat will make a popup above them
|
||||
show_player_mentions = true, --- @setting show_player_mentions weather a mentioned player will have a popup when mentioned in chat
|
||||
show_player_damage = true, --- @setting show_player_damage weather to show damage done by players
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
--- Used to config the spawn generation settings yes there is alot here i know just ignore the long tables at the end (they were generated with a command)
|
||||
-- @config Spawn-Area
|
||||
|
||||
local Features = require("modules/exp_scenario/features")
|
||||
local Feature = require("modules/exp_scenario/features")
|
||||
|
||||
return Features.config("spawn_area", {
|
||||
return Feature.register("spawn_area", {
|
||||
spawn_area = { --- @setting spawn_area Settings relating to the whole spawn area
|
||||
-- Enable predefined patches: 128, else: 32
|
||||
deconstruction_radius = 20, -- @setting deconstruction_radius All entities within this radius will be removed
|
||||
|
||||
@@ -3,17 +3,18 @@ Kicks players when all players on the server are afk
|
||||
]]
|
||||
|
||||
local Async = require("modules/exp_util/async")
|
||||
local Features = require("modules/exp_scenario/features")
|
||||
local Feature = require("modules/exp_scenario/features")
|
||||
local Roles = require("modules/exp_roles")
|
||||
local Storage = require("modules/exp_util/storage")
|
||||
local config = Features.config("afk_kick", {
|
||||
local feature = Feature.register("afk_kick", {
|
||||
admin_as_active = true, --- @setting admin_as_active When true admins will be treated as active regardless of afk time
|
||||
trust_as_active = true, --- @setting trust_as_active When true trusted players (by playtime) will be treated as active regardless of afk time
|
||||
active_role_id = Features.optional("number"), --- @setting active_role_id Players with this role or higher are treated as active regardless of afk time
|
||||
active_role_id = Feature.optional("number"), --- @setting active_role_id Players with this role or higher are treated as active regardless of afk time
|
||||
afk_minutes = 10, --- @setting afk_minutes The time in minutes that must pass for a player to be considered afk
|
||||
kick_minutes = 30, --- @setting kick_minutes The time in minutes that must pass without any active players for all players to be kicked
|
||||
trust_minutes = 600, --- @setting trust_minutes The time in minutes that a player must be online for to count as trusted
|
||||
})
|
||||
local config = feature.config
|
||||
|
||||
local ticks_per_minute = 3600
|
||||
|
||||
@@ -92,7 +93,7 @@ end
|
||||
|
||||
local e = defines.events
|
||||
|
||||
return Features.guard(config, {
|
||||
return feature:guard{
|
||||
events = {
|
||||
[e.on_player_joined_game] = on_player_joined_game,
|
||||
},
|
||||
@@ -100,4 +101,4 @@ return Features.guard(config, {
|
||||
[60 * 60 * 30] = check_afk_players,
|
||||
},
|
||||
has_active_player = has_active_player,
|
||||
})
|
||||
}
|
||||
@@ -2,9 +2,10 @@
|
||||
Adds auto replies to chat messages, as well as chat commands
|
||||
]]
|
||||
|
||||
local Features = require("modules/exp_scenario/features")
|
||||
local Feature = require("modules/exp_scenario/features")
|
||||
local Roles = require("modules/exp_roles")
|
||||
local config = require("modules/exp_scenario/config/chat_auto_reply")
|
||||
local feature = require("modules/exp_scenario/config/chat_auto_reply")
|
||||
local config = feature.config
|
||||
|
||||
local find = string.find
|
||||
local sub = string.sub
|
||||
@@ -57,8 +58,8 @@ end
|
||||
|
||||
local e = defines.events
|
||||
|
||||
return Features.guard(config, {
|
||||
return feature:guard{
|
||||
events = {
|
||||
[e.on_console_chat] = on_console_chat,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -3,8 +3,9 @@ Creates flying text entities when a player sends a message in chat
|
||||
]]
|
||||
|
||||
local FlyingText = require("modules/exp_util/flying_text")
|
||||
local Features = require("modules/exp_scenario/features")
|
||||
local config = require("modules/exp_scenario/config/popups")
|
||||
local Feature = require("modules/exp_scenario/features")
|
||||
local feature = require("modules/exp_scenario/config/popups")
|
||||
local config = feature.config
|
||||
|
||||
local lower = string.lower
|
||||
local find = string.find
|
||||
@@ -42,8 +43,8 @@ end
|
||||
|
||||
local e = defines.events
|
||||
|
||||
return Features.guard(config, {
|
||||
return feature:guard{
|
||||
events = {
|
||||
[e.on_console_chat] = on_console_chat,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -4,8 +4,9 @@ also shows player health when a player is attacked
|
||||
]]
|
||||
|
||||
local FlyingText = require("modules/exp_util/flying_text")
|
||||
local Features = require("modules/exp_scenario/features")
|
||||
local config = require("modules/exp_scenario/config/popups")
|
||||
local Feature = require("modules/exp_scenario/features")
|
||||
local feature = require("modules/exp_scenario/config/popups")
|
||||
local config = feature.config
|
||||
|
||||
local random = math.random
|
||||
local floor = math.floor
|
||||
@@ -45,8 +46,8 @@ end
|
||||
|
||||
local e = defines.events
|
||||
|
||||
return Features.guard(config, {
|
||||
return feature:guard{
|
||||
events = {
|
||||
[e.on_entity_damaged] = on_entity_damaged,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -3,10 +3,10 @@ Makes markers on the map where places have died and reclaims items if not recove
|
||||
]]
|
||||
|
||||
local ExpUtil = require("modules/exp_util")
|
||||
local Features = require("modules/exp_scenario/features")
|
||||
local Feature = require("modules/exp_scenario/features")
|
||||
local Storage = require("modules/exp_util/storage")
|
||||
|
||||
local config = Features.config("death_markers", {
|
||||
local feature = Feature.register("death_markers", {
|
||||
collect_corpses = true, --- @setting collect_corpses enables items being returned to the spawn point in chests upon corpse expiring
|
||||
show_map_markers = true, --- @setting show_map_markers shows markers on the map where bodies are
|
||||
clean_map_markers = false, --- @setting clean_map_markers removes the map marker once the body is gone
|
||||
@@ -15,6 +15,7 @@ local config = Features.config("death_markers", {
|
||||
show_light_at_corpse = true, --- @setting show_light_at_corpse if a light should be rendered at the corpse
|
||||
show_line_to_corpse = true, --- @setting show_line_to_corpse if a line should be rendered from you to your corpse
|
||||
})
|
||||
local config = feature.config
|
||||
|
||||
local map_tag_time_format = ExpUtil.format_time_factory{ format = "short", hours = true, minutes = true }
|
||||
|
||||
@@ -147,7 +148,7 @@ end
|
||||
|
||||
local e = defines.events
|
||||
|
||||
return Features.guard(config, {
|
||||
return feature:guard{
|
||||
on_nth_tick = {
|
||||
[60 * 60 * 5] = check_map_tags,
|
||||
},
|
||||
@@ -157,4 +158,4 @@ return Features.guard(config, {
|
||||
[e.on_player_respawned] = on_player_respawned,
|
||||
[e.on_character_corpse_expired] = on_character_corpse_expired,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -4,11 +4,11 @@ Sends alert messages to our discord server when certain events are triggered
|
||||
|
||||
local ExpUtil = require("modules/exp_util")
|
||||
local Colors = require("modules/exp_util/include/color")
|
||||
local Features = require("modules/exp_scenario/features")
|
||||
local Feature = require("modules/exp_scenario/features")
|
||||
local EntityProtection = require("modules/exp_scenario/control/protection")
|
||||
local Jail = require("modules/exp_scenario/control/jail")
|
||||
|
||||
local config = Features.config("discord_alerts", {
|
||||
local feature = Feature.register("discord_alerts", {
|
||||
show_playtime = true, --- @setting show_playtime Add the playtime of players to alerts
|
||||
entity_protection = true, --- @setting entity_protection Alert when a player repeatedly removes protected entities
|
||||
player_bans = true, --- @setting player_bans Alert when a player is banned or unbanned
|
||||
@@ -20,15 +20,7 @@ local config = Features.config("discord_alerts", {
|
||||
"config", "purge", "c", "command", "silent-command", "measured-command", "banlist", "permissions", "editor", "cheat",
|
||||
},
|
||||
})
|
||||
|
||||
--- Set of the logged commands, rebuilt when the config changes
|
||||
local logged_commands = {} --- @type table<string, true>
|
||||
Features.on_apply(config, function()
|
||||
logged_commands = {}
|
||||
for _, command in ipairs(config.logged_commands) do
|
||||
logged_commands[command] = true
|
||||
end
|
||||
end)
|
||||
local config = feature.config
|
||||
|
||||
--- Wrap an event handler so it only runs while the alert is enabled
|
||||
--- @param setting string
|
||||
@@ -267,7 +259,7 @@ end)
|
||||
events[e.on_console_command] = function(event)
|
||||
if event.player_index then
|
||||
local player_name = get_player_name(event)
|
||||
if logged_commands[event.command] then
|
||||
if Feature.to_set(config.logged_commands)[event.command] then
|
||||
emit_event{
|
||||
title = event.command:gsub("^%l", string.upper),
|
||||
description = "/" .. event.command .. " was used",
|
||||
@@ -281,6 +273,6 @@ events[e.on_console_command] = function(event)
|
||||
end
|
||||
end
|
||||
|
||||
return Features.guard(config, {
|
||||
return feature:guard{
|
||||
events = events,
|
||||
})
|
||||
}
|
||||
@@ -2,11 +2,13 @@
|
||||
Refreshes the gui of every player when a feature is enabled or disabled, see features.lua
|
||||
]]
|
||||
|
||||
local Features = require("modules/exp_scenario/features")
|
||||
local Feature = require("modules/exp_scenario/features")
|
||||
local Gui = require("modules/exp_gui")
|
||||
|
||||
--- Toolbar buttons of disabled features are hidden, which also hides their left elements
|
||||
local function on_feature_changed()
|
||||
--- @param event EventData.ExpScenario.on_config_updated
|
||||
local function on_config_updated(event)
|
||||
if event.path ~= "enabled" then return end
|
||||
for _, player in pairs(game.connected_players) do
|
||||
Gui._ensure_consistency{ player_index = player.index }
|
||||
end
|
||||
@@ -14,6 +16,6 @@ end
|
||||
|
||||
return {
|
||||
events = {
|
||||
[Features.events.on_feature_changed] = on_feature_changed,
|
||||
[Feature.events.on_config_updated] = on_config_updated,
|
||||
},
|
||||
}
|
||||
@@ -3,13 +3,14 @@ Marks mining drills for deconstruction when resources deplete
|
||||
]]
|
||||
|
||||
local Async = require("modules/exp_util/async")
|
||||
local Features = require("modules/exp_scenario/features")
|
||||
local Feature = require("modules/exp_scenario/features")
|
||||
|
||||
local config = Features.config("mine_depletion", {
|
||||
local feature = Feature.register("mine_depletion", {
|
||||
fluid = true, --- @setting fluid When true, checks for for fluid pipes when removing miners
|
||||
chest = true, --- @setting chest When true, checks for for chest when removing miners
|
||||
beacon = true, --- @setting beacon When true, checks for for beacon when removing miners
|
||||
})
|
||||
local config = feature.config
|
||||
|
||||
local floor = math.floor
|
||||
|
||||
@@ -276,8 +277,8 @@ end
|
||||
|
||||
local e = defines.events
|
||||
|
||||
return Features.guard(config, {
|
||||
return feature:guard{
|
||||
events = {
|
||||
[e.on_resource_depleted] = on_resource_depleted,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -3,22 +3,14 @@ Disable new players from having certain items in their inventory, most commonly
|
||||
]]
|
||||
|
||||
local ExpUtil = require("modules/exp_util")
|
||||
local Features = require("modules/exp_scenario/features")
|
||||
local Feature = require("modules/exp_scenario/features")
|
||||
local Roles = require("modules/exp_roles")
|
||||
|
||||
local config = Features.config("nuke_protection", {
|
||||
local feature = Feature.register("nuke_protection", {
|
||||
ignore_admins = true, --- @setting ignore_admins Admins can hold banned items
|
||||
banned_items = { "atomic-bomb" }, --- @setting banned_items Items which are removed from the inventory of players without the bypass permission
|
||||
})
|
||||
|
||||
--- Set of the banned items, rebuilt when the config changes
|
||||
local banned_items = {} --- @type table<string, true>
|
||||
Features.on_apply(config, function()
|
||||
banned_items = {}
|
||||
for _, item_name in ipairs(config.banned_items) do
|
||||
banned_items[item_name] = true
|
||||
end
|
||||
end)
|
||||
local config = feature.config
|
||||
|
||||
--- The inventories which are checked and the events which trigger the check
|
||||
local inventories = {
|
||||
@@ -36,6 +28,7 @@ local function check_items(player, type)
|
||||
if Roles.player_has_permission(player, "exp_scenario.bypass.nuke_protection") then return end
|
||||
if config.ignore_admins and player.admin then return end
|
||||
|
||||
local banned_items = Feature.to_set(config.banned_items)
|
||||
local items = {} --- @type LuaItemStack[]
|
||||
local inventory = assert(player.get_inventory(type))
|
||||
-- Check what items the player has
|
||||
@@ -68,6 +61,6 @@ for event_id, inventory in pairs(inventories) do
|
||||
end
|
||||
end
|
||||
|
||||
return Features.guard(config, {
|
||||
return feature:guard{
|
||||
events = events,
|
||||
})
|
||||
}
|
||||
@@ -3,10 +3,10 @@ Protects entities and areas from being mined by players other than the one who p
|
||||
]]
|
||||
|
||||
local Storage = require("modules/exp_util/storage")
|
||||
local Features = require("modules/exp_scenario/features")
|
||||
local Feature = require("modules/exp_scenario/features")
|
||||
local Roles = require("modules/exp_roles")
|
||||
|
||||
local config = Features.config("protection", {
|
||||
local feature = Feature.register("protection", {
|
||||
ignore_admins = true, --- @setting ignore_admins If admins are ignored by the protection filter
|
||||
repeat_count = 5, --- @setting repeat_count Number of protected entities that must be removed within repeat_minutes in order to trigger repeated removal protection
|
||||
repeat_minutes = 20, --- @setting repeat_minutes The length of time, in minutes, that protected removals will be remembered for
|
||||
@@ -19,6 +19,7 @@ local config = Features.config("protection", {
|
||||
"reactor", "fusion-reactor", "rocket-silo",
|
||||
},
|
||||
})
|
||||
local config = feature.config
|
||||
|
||||
local format_string = string.format
|
||||
local floor = math.floor
|
||||
@@ -31,12 +32,6 @@ local Protection = {
|
||||
--- Raised when a player mines protected entities repeatedly, or one which always counts as repeated
|
||||
--- @type EventData.ExpScenario_Protection.on_repeat_violation
|
||||
on_repeat_violation = script.generate_event_name(),
|
||||
--- Names of entities which are always protected
|
||||
--- @type string[]
|
||||
protected_entity_names = {},
|
||||
--- Types of entities which are always protected
|
||||
--- @type string[]
|
||||
protected_entity_types = {},
|
||||
--- @package
|
||||
events = {},
|
||||
--- @package
|
||||
@@ -50,28 +45,17 @@ local Protection = {
|
||||
--- @field last uint Tick of the last protected removal
|
||||
--- @field count number Protected removals since the last repeat violation
|
||||
|
||||
--- @param values string[]
|
||||
--- @return table<string, true>
|
||||
local function to_set(values)
|
||||
local set = {}
|
||||
for _, value in ipairs(values) do
|
||||
set[value] = true
|
||||
end
|
||||
return set
|
||||
--- Names of entities which are always protected
|
||||
--- @return string[]
|
||||
function Protection.get_protected_entity_names()
|
||||
return config.always_protected_names
|
||||
end
|
||||
|
||||
local always_protected_names = {} --- @type table<string, true>
|
||||
local always_protected_types = {} --- @type table<string, true>
|
||||
local always_trigger_repeat_names = {} --- @type table<string, true>
|
||||
local always_trigger_repeat_types = {} --- @type table<string, true>
|
||||
Features.on_apply(config, function()
|
||||
Protection.protected_entity_names = config.always_protected_names
|
||||
Protection.protected_entity_types = config.always_protected_types
|
||||
always_protected_names = to_set(config.always_protected_names)
|
||||
always_protected_types = to_set(config.always_protected_types)
|
||||
always_trigger_repeat_names = to_set(config.always_trigger_repeat_names)
|
||||
always_trigger_repeat_types = to_set(config.always_trigger_repeat_types)
|
||||
end)
|
||||
--- Types of entities which are always protected
|
||||
--- @return string[]
|
||||
function Protection.get_protected_entity_types()
|
||||
return config.always_protected_types
|
||||
end
|
||||
|
||||
local protected_entities = {} --- @type table<uint, table<string, LuaEntity>> Keyed by surface index then entity key
|
||||
local protected_areas = {} --- @type table<uint, table<string, BoundingBox>> Keyed by surface index then area key
|
||||
@@ -131,7 +115,7 @@ end
|
||||
--- @param entity LuaEntity
|
||||
--- @return boolean
|
||||
function Protection.is_entity_protected(entity)
|
||||
if always_protected_names[entity.name] or always_protected_types[entity.type] then return true end
|
||||
if Feature.to_set(config.always_protected_names)[entity.name] or Feature.to_set(config.always_protected_types)[entity.type] then return true end
|
||||
local entities = protected_entities[entity.surface.index]
|
||||
if not entities then return false end
|
||||
return entities[Protection.get_entity_key(entity)] == entity
|
||||
@@ -209,7 +193,7 @@ local function raise_violation(event, player)
|
||||
script.raise_event(Protection.on_player_mined_protected, event)
|
||||
|
||||
local entity = event.entity
|
||||
local always_repeat = always_trigger_repeat_names[entity.name] or always_trigger_repeat_types[entity.type]
|
||||
local always_repeat = Feature.to_set(config.always_trigger_repeat_names)[entity.name] or Feature.to_set(config.always_trigger_repeat_types)[entity.type]
|
||||
if always_repeat or player_repeats.count >= config.repeat_count then
|
||||
player_repeats.count = 0
|
||||
script.raise_event(Protection.on_repeat_violation, event)
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
Adds a custom spawn area with chests and afk turrets
|
||||
]]
|
||||
|
||||
local Features = require("modules/exp_scenario/features")
|
||||
local config = require("modules/exp_scenario/config/spawn_area")
|
||||
local Feature = require("modules/exp_scenario/features")
|
||||
local feature = require("modules/exp_scenario/config/spawn_area")
|
||||
local config = feature.config
|
||||
|
||||
--- Apply an offset to a LuaPosition
|
||||
--- @param position MapPosition.struct
|
||||
@@ -299,9 +300,9 @@ end
|
||||
|
||||
local e = defines.events
|
||||
|
||||
return Features.guard(config, {
|
||||
return feature:guard{
|
||||
on_nth_tick = on_nth_tick,
|
||||
events = {
|
||||
[e.on_player_created] = on_player_created,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -2,13 +2,14 @@
|
||||
Automatically name stations when they are placed based on closest resource and direction from spawn
|
||||
]]
|
||||
|
||||
local Features = require("modules/exp_scenario/features")
|
||||
local Feature = require("modules/exp_scenario/features")
|
||||
|
||||
local config = Features.config("station_auto_name", {
|
||||
local feature = Feature.register("station_auto_name", {
|
||||
--- @setting station_name the name given to new stations, placeholders:
|
||||
-- __icon__ __item_name__ __backer_name__ __direction__ __x__ __y__
|
||||
station_name = "[L] __icon__",
|
||||
})
|
||||
local config = feature.config
|
||||
|
||||
local get_direction do
|
||||
local directions = {
|
||||
@@ -104,9 +105,9 @@ end
|
||||
|
||||
local e = defines.events
|
||||
|
||||
return Features.guard(config, {
|
||||
return feature:guard{
|
||||
events = {
|
||||
[e.on_built_entity] = rename_station,
|
||||
[e.on_robot_built_entity] = rename_station,
|
||||
},
|
||||
})
|
||||
}
|
||||
+180
-167
@@ -1,141 +1,111 @@
|
||||
--[[-- ExpScenario - Features
|
||||
Features which the controller can enable, disable, and configure while the game is running
|
||||
|
||||
A feature's config is the table returned by Features.config. The controller sends
|
||||
overrides over rcon, these are kept in storage and copied onto that same table,
|
||||
A feature's config table is changed in place when the controller sends overrides,
|
||||
so read values when they are used rather than copying them into locals when the
|
||||
file loads. Values derived from the config can be rebuilt with Features.on_apply.
|
||||
file loads. Booleans, numbers, strings, and lists of strings can be overridden,
|
||||
including those in nested tables which are addressed as "section.key". Use
|
||||
Feature.optional for a value which has no default. Values used while the file
|
||||
loads, such as on_nth_tick periods, stay as plain values and are not listed in
|
||||
features.ts.
|
||||
|
||||
Booleans, numbers, strings, and lists of strings can be overridden, including
|
||||
those in nested tables which are addressed as "section.key". Use Features.optional
|
||||
for a value which has no default. Values used while the file loads, such as
|
||||
on_nth_tick periods, stay as plain values and are not listed in features.ts.
|
||||
|
||||
--- Declaring a feature with config, small configs live at the top of the file which uses them:
|
||||
local Features = require("modules/exp_scenario/features")
|
||||
local config = Features.config("my_feature", {
|
||||
--- Registering a feature, small configs live at the top of the file which uses them:
|
||||
local Feature = require("modules/exp_scenario/features")
|
||||
local feature = Feature.register("my_feature", {
|
||||
show_message = true,
|
||||
role_id = Features.optional("number"),
|
||||
role_id = Feature.optional("number"),
|
||||
})
|
||||
local config = feature.config
|
||||
|
||||
--- Handlers do nothing while the feature is disabled, a feature without config can be guarded by name:
|
||||
return Features.guard(config, {
|
||||
--- Handlers do nothing while the feature is disabled:
|
||||
return feature:guard{
|
||||
events = { [defines.events.on_player_joined_game] = on_player_joined_game },
|
||||
})
|
||||
}
|
||||
|
||||
--- Toolbar buttons are hidden while disabled, which also hides their left element:
|
||||
Gui.toolbar.create_button{
|
||||
visible = function(player, element) return Features.is_enabled("my_feature") end,
|
||||
visible = function(player, element) return feature:is_enabled() end,
|
||||
}
|
||||
|
||||
--- Commands are denied and left out of help while disabled:
|
||||
Commands.new("my_command", { "..." }):add_flags{ feature = "my_feature" }
|
||||
Commands.new("my_command", { "..." }):add_flags{ feature = feature }
|
||||
|
||||
--- Changes are raised per value, values derived from a list can use Feature.to_set:
|
||||
[Feature.events.on_config_updated] = function(event) end, -- { feature_name, path, old_value, new_value }
|
||||
if Feature.to_set(config.names)[name] then end
|
||||
]]
|
||||
|
||||
local Storage = require("modules/exp_util/storage")
|
||||
|
||||
--- @class ExpScenario.Features
|
||||
local Features = {
|
||||
events = {
|
||||
--- Raised when the controller changes a feature, gui and commands are refreshed by control/features.lua
|
||||
on_feature_changed = script.generate_event_name(),
|
||||
},
|
||||
}
|
||||
|
||||
--- @class ExpScenario.FeatureConfig
|
||||
--- @field enabled boolean
|
||||
|
||||
--- @alias ExpScenario.Feature string | ExpScenario.FeatureConfig
|
||||
|
||||
--- @class ExpScenario.FeatureField
|
||||
--- @field parent table The table holding the value within the config
|
||||
--- @field key string The key of the value within its parent
|
||||
--- @field default any
|
||||
--- @field type string "boolean", "number", "string", "list" of strings, or a type which can not be overridden
|
||||
|
||||
--- @class ExpScenario.Optional
|
||||
--- @field __feature_optional string
|
||||
--- @class ExpScenario.Feature
|
||||
--- @field name string
|
||||
--- @field config table Changed in place when the config is updated
|
||||
--- @field fields table<string, ExpScenario.FeatureField> The values in the config by path
|
||||
--- @field overrides table<string, any> The overrides from the controller by path, restored from storage on load
|
||||
local Feature = {
|
||||
_registered = {}, --- @type table<string, ExpScenario.Feature>
|
||||
events = {
|
||||
--- Raised for each value which changes when the controller updates a feature
|
||||
--- @type EventData.ExpScenario.on_config_updated
|
||||
on_config_updated = script.generate_event_name(),
|
||||
},
|
||||
}
|
||||
|
||||
--- The live config tables by feature name
|
||||
local features = {} --- @type table<string, ExpScenario.FeatureConfig>
|
||||
--- @class EventData.ExpScenario.on_config_updated : EventData
|
||||
--- @field feature_name string
|
||||
--- @field path string The path of the value within the config, "enabled" when the feature is enabled or disabled
|
||||
--- @field old_value any
|
||||
--- @field new_value any
|
||||
|
||||
--- The values of each feature by feature name then path
|
||||
local fields = {} --- @type table<string, table<string, ExpScenario.FeatureField>>
|
||||
local Feature_mt = {
|
||||
__index = Feature,
|
||||
}
|
||||
|
||||
--- Called after a feature is applied by feature name
|
||||
local on_apply = {} --- @type table<string, fun(config: table)[]>
|
||||
|
||||
--- Overrides received from the controller by feature name then path
|
||||
local overrides = {} --- @type table<string, table<string, boolean | number | string | string[]>>
|
||||
|
||||
--- Check if a table is a list, empty tables count as lists
|
||||
--- @param tbl table
|
||||
--- @return boolean
|
||||
local function is_list(tbl)
|
||||
return next(tbl) == nil or tbl[1] ~= nil
|
||||
end
|
||||
--- The overrides of each feature by name, only these are stored because the rest is rebuilt when the file loads
|
||||
local stored_overrides = {} --- @type table<string, table<string, any>>
|
||||
Storage.register(stored_overrides, function(tbl)
|
||||
stored_overrides = tbl
|
||||
for name, feature in pairs(Feature._registered) do
|
||||
feature.overrides = tbl[name] or {}
|
||||
feature:_apply()
|
||||
end
|
||||
end)
|
||||
|
||||
--- Check if a table is a list of strings, empty tables count as lists of strings
|
||||
--- @param tbl table
|
||||
--- @return boolean
|
||||
local function is_string_list(tbl)
|
||||
if not is_list(tbl) then return false end
|
||||
if next(tbl) ~= nil and tbl[1] == nil then return false end
|
||||
for _, item in pairs(tbl) do
|
||||
if type(item) ~= "string" then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- Check if an override can replace a field
|
||||
--- @param field ExpScenario.FeatureField
|
||||
--- @param value any
|
||||
--- Check if two values are equal, lists are compared by their items
|
||||
--- @param a any
|
||||
--- @param b any
|
||||
--- @return boolean
|
||||
local function is_valid(field, value)
|
||||
if field.type == "list" then
|
||||
return type(value) == "table" and is_string_list(value)
|
||||
local function values_equal(a, b)
|
||||
if type(a) ~= "table" or type(b) ~= "table" then return a == b end
|
||||
if #a ~= #b then return false end
|
||||
for index, item in ipairs(a) do
|
||||
if b[index] ~= item then return false end
|
||||
end
|
||||
return type(value) == field.type and (field.type == "boolean" or field.type == "number" or field.type == "string")
|
||||
end
|
||||
|
||||
--- Reset a feature to its defaults and apply its overrides
|
||||
--- @param name string
|
||||
local function apply(name)
|
||||
local feature_overrides = overrides[name] or {}
|
||||
for path, field in pairs(fields[name]) do
|
||||
local value = feature_overrides[path]
|
||||
if value == nil then
|
||||
value = field.default
|
||||
elseif not is_valid(field, value) then
|
||||
log("[WARNING] Invalid override for " .. name .. "." .. path .. ", using the default")
|
||||
value = field.default
|
||||
end
|
||||
field.parent[field.key] = value
|
||||
end
|
||||
|
||||
for _, callback in ipairs(on_apply[name]) do
|
||||
callback(features[name])
|
||||
end
|
||||
end
|
||||
|
||||
Storage.register(overrides, function(tbl)
|
||||
overrides = tbl
|
||||
for name in pairs(features) do
|
||||
apply(name)
|
||||
end
|
||||
end)
|
||||
|
||||
--- Mark a value as having no default, the argument is the type it can be overridden with
|
||||
--- @param value_type "boolean" | "number" | "string" | "list"
|
||||
--- @return any
|
||||
function Features.optional(value_type)
|
||||
return { __feature_optional = value_type }
|
||||
return true
|
||||
end
|
||||
|
||||
--- Collect the values of a config table, recursing into nested tables which are not lists
|
||||
--- @param feature_fields table<string, ExpScenario.FeatureField>
|
||||
--- @param fields table<string, ExpScenario.FeatureField>
|
||||
--- @param tbl table
|
||||
--- @param prefix string
|
||||
local function collect_fields(feature_fields, tbl, prefix)
|
||||
local function collect_fields(fields, tbl, prefix)
|
||||
for key, value in pairs(tbl) do
|
||||
if type(key) == "string" then
|
||||
local field = { parent = tbl, key = key, default = value, type = type(value) }
|
||||
@@ -146,95 +116,97 @@ local function collect_fields(feature_fields, tbl, prefix)
|
||||
tbl[key] = nil
|
||||
elseif is_string_list(value) then
|
||||
field.type = "list"
|
||||
elseif is_list(value) then
|
||||
elseif value[1] ~= nil then
|
||||
field.type = "table"
|
||||
else
|
||||
collect_fields(feature_fields, value, prefix .. key .. ".")
|
||||
collect_fields(fields, value, prefix .. key .. ".")
|
||||
field = nil
|
||||
end
|
||||
end
|
||||
if field then
|
||||
feature_fields[prefix .. key] = field
|
||||
fields[prefix .. key] = field
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Declare a feature and its config, features are enabled unless the config says otherwise
|
||||
--- @generic T : table
|
||||
--- Mark a value as having no default, the argument is the type it can be overridden with
|
||||
--- @param value_type "boolean" | "number" | "string" | "list"
|
||||
--- @return any
|
||||
function Feature.optional(value_type)
|
||||
return { __feature_optional = value_type }
|
||||
end
|
||||
|
||||
--- Register a feature and its config, features are enabled unless the config says otherwise
|
||||
--- @param name string
|
||||
--- @param config T?
|
||||
--- @return T
|
||||
function Features.config(name, config)
|
||||
assert(features[name] == nil, "Feature already declared: " .. name)
|
||||
config = config or {} --[[@as T]]
|
||||
--- @param config table?
|
||||
--- @return ExpScenario.Feature
|
||||
function Feature.register(name, config)
|
||||
assert(Feature._registered[name] == nil, "Feature already registered: " .. name)
|
||||
config = config or {}
|
||||
if config.enabled == nil then
|
||||
config.enabled = true
|
||||
end
|
||||
|
||||
local feature_fields = {}
|
||||
collect_fields(feature_fields, config, "")
|
||||
local fields = {}
|
||||
collect_fields(fields, config, "")
|
||||
|
||||
features[name] = config
|
||||
fields[name] = feature_fields
|
||||
on_apply[name] = {}
|
||||
return config
|
||||
local feature = setmetatable({
|
||||
name = name,
|
||||
config = config,
|
||||
fields = fields,
|
||||
overrides = {},
|
||||
}, Feature_mt) --[[@as ExpScenario.Feature]]
|
||||
|
||||
Feature._registered[name] = feature
|
||||
return feature
|
||||
end
|
||||
|
||||
--- Get the config of a feature from its name, or return the config given
|
||||
--- @param feature ExpScenario.Feature
|
||||
--- @return ExpScenario.FeatureConfig
|
||||
local function resolve(feature)
|
||||
if type(feature) == "table" then
|
||||
return feature
|
||||
end
|
||||
return features[feature] or error("Unknown feature: " .. tostring(feature), 3)
|
||||
--- Get a registered feature by name
|
||||
--- @param name string
|
||||
--- @return ExpScenario.Feature
|
||||
function Feature.get(name)
|
||||
return Feature._registered[name] or error("Unknown feature: " .. tostring(name), 2)
|
||||
end
|
||||
|
||||
--- Get the name of a feature from its config, or return the name given
|
||||
--- @param feature ExpScenario.Feature
|
||||
--- @return string
|
||||
local function resolve_name(feature)
|
||||
if type(feature) == "string" then
|
||||
return feature
|
||||
end
|
||||
for name, config in pairs(features) do
|
||||
if config == feature then
|
||||
return name
|
||||
end
|
||||
end
|
||||
error("Unknown feature config", 3)
|
||||
end
|
||||
|
||||
--- Check if a feature is enabled
|
||||
--- @param feature ExpScenario.Feature
|
||||
--- Check if a field can take a value
|
||||
--- @param field ExpScenario.FeatureField
|
||||
--- @param value any
|
||||
--- @return boolean
|
||||
function Features.is_enabled(feature)
|
||||
return resolve(feature).enabled
|
||||
local function is_valid(field, value)
|
||||
if field.type == "list" then
|
||||
return type(value) == "table" and is_string_list(value)
|
||||
end
|
||||
return type(value) == field.type and (field.type == "boolean" or field.type == "number" or field.type == "string")
|
||||
end
|
||||
|
||||
--- Call a function now and whenever the config of a feature changes, used to rebuild values derived from it
|
||||
-- Also called during on_load, so it must only change locals and not the game state
|
||||
--- @param feature ExpScenario.Feature
|
||||
--- @param callback fun(config: any)
|
||||
function Features.on_apply(feature, callback)
|
||||
local name = resolve_name(feature)
|
||||
table.insert(on_apply[name], callback)
|
||||
callback(features[name])
|
||||
--- Set every value to its override, or its default when there is no valid override
|
||||
--- @package
|
||||
function Feature:_apply()
|
||||
for path, field in pairs(self.fields) do
|
||||
local value = self.overrides[path]
|
||||
if value == nil then
|
||||
value = field.default
|
||||
elseif not is_valid(field, value) then
|
||||
log("[WARNING] Invalid override for " .. self.name .. "." .. path .. ", using the default")
|
||||
value = field.default
|
||||
end
|
||||
field.parent[field.key] = value
|
||||
end
|
||||
end
|
||||
|
||||
--- Check if the feature is enabled
|
||||
--- @return boolean
|
||||
function Feature:is_enabled()
|
||||
return self.config.enabled
|
||||
end
|
||||
|
||||
--- Make the event and on_nth_tick handlers of an event handler lib do nothing while the feature is disabled
|
||||
-- A name which has not been declared is declared without config
|
||||
--- @generic T : table
|
||||
--- @param feature ExpScenario.Feature
|
||||
--- @param lib T
|
||||
--- @return T
|
||||
function Features.guard(feature, lib)
|
||||
if type(feature) == "string" and features[feature] == nil then
|
||||
Features.config(feature)
|
||||
end
|
||||
local config = resolve(feature)
|
||||
|
||||
function Feature:guard(lib)
|
||||
local config = self.config
|
||||
for _, handlers in pairs{ lib.events or {}, lib.on_nth_tick or {} } do
|
||||
for key, handler in pairs(handlers) do
|
||||
handlers[key] = function(event)
|
||||
@@ -248,6 +220,58 @@ function Features.guard(feature, lib)
|
||||
return lib
|
||||
end
|
||||
|
||||
--- Replace the overrides of the feature, then raise on_config_updated for each value which changed
|
||||
--- @param enabled boolean
|
||||
--- @param values table<string, boolean | number | string | string[]>
|
||||
function Feature:update_config(enabled, values)
|
||||
local overrides = { enabled = enabled }
|
||||
for path, value in pairs(values) do
|
||||
if self.fields[path] == nil then
|
||||
log("[WARNING] Unknown override for " .. self.name .. "." .. path)
|
||||
end
|
||||
overrides[path] = value
|
||||
end
|
||||
|
||||
local old_values = {}
|
||||
for path, field in pairs(self.fields) do
|
||||
old_values[path] = field.parent[field.key]
|
||||
end
|
||||
|
||||
self.overrides = overrides
|
||||
stored_overrides[self.name] = overrides
|
||||
self:_apply()
|
||||
|
||||
for path, field in pairs(self.fields) do
|
||||
local new_value = field.parent[field.key]
|
||||
if not values_equal(old_values[path], new_value) then
|
||||
script.raise_event(Feature.events.on_config_updated, {
|
||||
feature_name = self.name,
|
||||
path = path,
|
||||
old_value = old_values[path],
|
||||
new_value = new_value,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Sets made from lists in the config, keyed by the list so a replaced list gets a new set
|
||||
local sets = setmetatable({}, { __mode = "k" }) --- @type table<string[], table<string, true>>
|
||||
|
||||
--- Get a lookup set of the items in a list, cached until the list is replaced
|
||||
--- @param list string[]
|
||||
--- @return table<string, true>
|
||||
function Feature.to_set(list)
|
||||
local set = sets[list]
|
||||
if not set then
|
||||
set = {}
|
||||
for _, item in ipairs(list) do
|
||||
set[item] = true
|
||||
end
|
||||
sets[list] = set
|
||||
end
|
||||
return set
|
||||
end
|
||||
|
||||
--- @class ExpScenario.FeatureUpdate
|
||||
--- @field name string
|
||||
--- @field enabled boolean
|
||||
@@ -255,24 +279,13 @@ end
|
||||
|
||||
--- Called over rcon by the instance plugin with the features which changed, unknown features are ignored
|
||||
--- @param updates ExpScenario.FeatureUpdate[]
|
||||
function Features.receive_update(updates)
|
||||
function Feature.receive_update(updates)
|
||||
for _, update in ipairs(updates) do
|
||||
local name = update.name
|
||||
local feature_fields = fields[name]
|
||||
if feature_fields then
|
||||
local override = { enabled = update.enabled }
|
||||
for path, value in pairs(update.values) do
|
||||
if feature_fields[path] == nil then
|
||||
log("[WARNING] Unknown override for " .. name .. "." .. path)
|
||||
end
|
||||
override[path] = value
|
||||
end
|
||||
|
||||
overrides[name] = override
|
||||
apply(name)
|
||||
script.raise_event(Features.events.on_feature_changed, { feature_name = name })
|
||||
local feature = Feature._registered[update.name]
|
||||
if feature then
|
||||
feature:update_config(update.enabled, update.values)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return Features
|
||||
return Feature
|
||||
@@ -2,10 +2,12 @@
|
||||
Adds a config menu for setting autofill of placed entities
|
||||
]]
|
||||
|
||||
local Features = require("modules/exp_scenario/features")
|
||||
local Feature = require("modules/exp_scenario/features")
|
||||
local Gui = require("modules/exp_gui")
|
||||
local Roles = require("modules/exp_roles")
|
||||
local config = require("modules.exp_legacy.config.gui.autofill")
|
||||
|
||||
local feature = Feature.register("autofill")
|
||||
local FlyingText = require("modules/exp_util/flying_text")
|
||||
|
||||
local min = math.min
|
||||
@@ -331,7 +333,7 @@ Gui.toolbar.create_button{
|
||||
sprite = config.icon,
|
||||
tooltip = { "exp-gui_autofill.tooltip-main" },
|
||||
visible = function(player, element)
|
||||
return Features.is_enabled("autofill") and Roles.player_has_permission(player, "exp_scenario.gui.autofill")
|
||||
return feature:is_enabled() and Roles.player_has_permission(player, "exp_scenario.gui.autofill")
|
||||
end
|
||||
}
|
||||
|
||||
@@ -397,9 +399,9 @@ end
|
||||
|
||||
local e = defines.events
|
||||
|
||||
return Features.guard("autofill", {
|
||||
return feature:guard{
|
||||
elements = Elements,
|
||||
events = {
|
||||
[e.on_built_entity] = on_built_entity,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -4,7 +4,7 @@ Adds a few buttons for common actions
|
||||
|
||||
local Gui = require("modules/exp_gui")
|
||||
local Commands = require("modules/exp_commands")
|
||||
local Features = require("modules/exp_scenario/features")
|
||||
local Feature = require("modules/exp_scenario/features")
|
||||
local Roles = require("modules/exp_roles")
|
||||
|
||||
local addon_artillery = require("modules/exp_scenario/commands/artillery")
|
||||
@@ -128,7 +128,9 @@ local function on_role_changed(event)
|
||||
end
|
||||
|
||||
--- Actions for commands of a disabled feature are hidden
|
||||
local function on_feature_changed()
|
||||
--- @param event EventData.ExpScenario.on_config_updated
|
||||
local function on_config_updated(event)
|
||||
if event.path ~= "enabled" then return end
|
||||
for _, player in pairs(game.connected_players) do
|
||||
Elements.container.refresh_player(player)
|
||||
end
|
||||
@@ -138,6 +140,6 @@ return {
|
||||
elements = Elements,
|
||||
events = {
|
||||
[Roles.events.on_player_roles_changed] = on_role_changed,
|
||||
[Features.events.on_feature_changed] = on_feature_changed,
|
||||
[Feature.events.on_config_updated] = on_config_updated,
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,8 @@ function luaFiles(dir, prefix = "") {
|
||||
const moduleFiles = luaFiles(path.join(import.meta.dirname, "..", "module"));
|
||||
|
||||
/**
|
||||
* Read a default from a Features.config call, a path of "a.b" looks for b after the table a starts.
|
||||
* Only covers booleans, numbers, strings, lists of strings, and Features.optional.
|
||||
* Read a default from a Feature.register call, a path of "a.b" looks for b after the table a starts.
|
||||
* Only covers booleans, numbers, strings, lists of strings, and Feature.optional.
|
||||
*/
|
||||
function luaDefault(source, path) {
|
||||
const keys = path.split(".");
|
||||
@@ -32,13 +32,13 @@ function luaDefault(source, path) {
|
||||
}
|
||||
|
||||
const key = keys.at(-1);
|
||||
const match = new RegExp(`^\\s*${key} = (true|false|-?[\\d.]+|"[^"]*"|Features\\.optional\\("\\w+"\\)|\\{[^{}]*\\})`, "m")
|
||||
const match = new RegExp(`^\\s*${key} = (true|false|-?[\\d.]+|"[^"]*"|Feature\\.optional\\("\\w+"\\)|\\{[^{}]*\\})`, "m")
|
||||
.exec(source.slice(start));
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
const literal = match[1];
|
||||
if (literal.startsWith("Features.optional")) {
|
||||
if (literal.startsWith("Feature.optional")) {
|
||||
return null;
|
||||
}
|
||||
if (literal.startsWith("{")) {
|
||||
@@ -51,7 +51,7 @@ t.test("features", t2 => {
|
||||
for (const feature of features) {
|
||||
t2.test(feature.name, t3 => {
|
||||
const declarations = moduleFiles.filter(([, source]) => (
|
||||
source.includes(`Features.config("${feature.name}"`) || source.includes(`Features.guard("${feature.name}"`)
|
||||
source.includes(`Feature.register("${feature.name}"`)
|
||||
));
|
||||
t3.equal(declarations.length, 1, "one lua file declares the feature");
|
||||
const source = declarations[0]?.[1] ?? "";
|
||||
@@ -92,8 +92,8 @@ t.test("validateFeatureValues()", t2 => {
|
||||
{ message: "Setting always_protected_types of protection must be a list of strings" },
|
||||
);
|
||||
t2.throws(
|
||||
() => validateFeatureValues("afk_kick", { active_role_id: 1.5 }),
|
||||
{ message: "Setting active_role_id of afk_kick must be a role id or null" },
|
||||
() => validateFeatureValues("afk_kick", { afk_minutes: null }),
|
||||
{ message: "Setting afk_minutes of afk_kick can not be null" },
|
||||
);
|
||||
t2.same(
|
||||
validateFeatureValues("protection", { always_protected_types: ["boiler"], always_trigger_repeat_types: ["reactor", "fusion-reactor", "rocket-silo"] }),
|
||||
|
||||
@@ -13,10 +13,10 @@ local plugin_root = assert(source:match("^@(.*)/test/module/env%.lua$"))
|
||||
local Framework = assert(loadfile(shared_root .. "/framework.lua"))() --- @type Framework
|
||||
|
||||
--- @class ExpScenario.TestEnv : Stubs
|
||||
--- @field Features ExpScenario.Features A fresh copy of the features module
|
||||
--- @field Feature ExpScenario.Feature A fresh copy of the features module
|
||||
--- @field storage table The table registered with storage, as on_init would store it
|
||||
--- @field on_load fun(tbl: table) Act out on_load with the given storage table
|
||||
--- @field load_config fun(name: string): table Load module/config/<name>.lua
|
||||
--- @field load_config fun(name: string): ExpScenario.Feature Load module/config/<name>.lua
|
||||
--- @field logged string[] Messages passed to log
|
||||
|
||||
return Framework.suite(function(env)
|
||||
@@ -33,8 +33,8 @@ return Framework.suite(function(env)
|
||||
},
|
||||
}
|
||||
|
||||
env.Features = assert(loadfile(plugin_root .. "/module/features.lua"))()
|
||||
env.extend_requires{ ["modules/exp_scenario/features"] = env.Features }
|
||||
env.Feature = assert(loadfile(plugin_root .. "/module/features.lua"))()
|
||||
env.extend_requires{ ["modules/exp_scenario/features"] = env.Feature }
|
||||
|
||||
function env.on_load(tbl)
|
||||
env.storage = tbl
|
||||
|
||||
@@ -1,137 +1,140 @@
|
||||
local Suite = ... --- @type Suite<ExpScenario.TestEnv>
|
||||
|
||||
Suite.test("config() enables features unless the defaults say otherwise", function(env)
|
||||
Suite.eq(env.Features.config("on", { value = 1 }).enabled, true, "enabled by default")
|
||||
Suite.eq(env.Features.config("off", { enabled = false }).enabled, false, "the defaults can disable it")
|
||||
Suite.eq(env.Features.config("empty"), { enabled = true }, "config is optional")
|
||||
Suite.throws(function() env.Features.config("on", {}) end, "Feature already declared: on", "names are unique")
|
||||
--- The values of on_config_updated events, ignoring the event name and tick
|
||||
local function updates(env)
|
||||
local rtn = {}
|
||||
for _, event in ipairs(env.events) do
|
||||
rtn[#rtn + 1] = { event.feature_name, event.path, event.old_value, event.new_value }
|
||||
end
|
||||
return rtn
|
||||
end
|
||||
|
||||
Suite.test("register() enables features unless the config says otherwise", function(env)
|
||||
Suite.eq(env.Feature.register("on", { value = 1 }).config.enabled, true, "enabled by default")
|
||||
Suite.eq(env.Feature.register("off", { enabled = false }):is_enabled(), false, "the config can disable it")
|
||||
Suite.eq(env.Feature.register("empty").config, { enabled = true }, "config is optional")
|
||||
Suite.throws(function() env.Feature.register("on", {}) end, "Feature already registered: on", "names are unique")
|
||||
end)
|
||||
|
||||
Suite.test("is_enabled() takes a name or a config", function(env)
|
||||
local config = env.Features.config("test", { enabled = false })
|
||||
Suite.eq(env.Features.is_enabled("test"), false, "by name")
|
||||
Suite.eq(env.Features.is_enabled(config), false, "by config")
|
||||
Suite.throws(function() env.Features.is_enabled("unknown") end, "Unknown feature: unknown", "unknown names error")
|
||||
Suite.test("get() finds registered features", function(env)
|
||||
local feature = env.Feature.register("test")
|
||||
Suite.eq(env.Feature.get("test") == feature, true, "by name")
|
||||
Suite.throws(function() env.Feature.get("unknown") end, "Unknown feature: unknown", "unknown names error")
|
||||
end)
|
||||
|
||||
Suite.test("receive_update() changes the config in place", function(env)
|
||||
local config = env.Features.config("test", { flag = true, count = 5, text = "a" })
|
||||
env.Features.receive_update{ { name = "test", enabled = false, values = { flag = false, count = 7 } } }
|
||||
Suite.test("update_config() changes the config in place", function(env)
|
||||
local feature = env.Feature.register("test", { flag = true, count = 5, text = "a" })
|
||||
local config = feature.config
|
||||
feature:update_config(false, { flag = false, count = 7 })
|
||||
Suite.eq(config, { enabled = false, flag = false, count = 7, text = "a" }, "given values replace the defaults")
|
||||
|
||||
env.Features.receive_update{ { name = "test", enabled = true, values = { text = "b" } } }
|
||||
feature:update_config(true, { text = "b" })
|
||||
Suite.eq(config, { enabled = true, flag = true, count = 5, text = "b" }, "values left out go back to their default")
|
||||
Suite.eq(env.storage, { test = { enabled = true, text = "b" } }, "the overrides are kept in storage")
|
||||
end)
|
||||
|
||||
Suite.test("receive_update() raises on_feature_changed for each known feature", function(env)
|
||||
env.Features.config("test")
|
||||
env.Features.receive_update{
|
||||
{ name = "test", enabled = false, values = {} },
|
||||
{ name = "unknown", enabled = false, values = {} },
|
||||
}
|
||||
Suite.eq(env.events, {
|
||||
{ name = env.Features.events.on_feature_changed, tick = game.tick, feature_name = "test" },
|
||||
}, "raised once")
|
||||
Suite.test("update_config() raises on_config_updated for each changed value", function(env)
|
||||
local feature = env.Feature.register("test", { flag = true, names = { "a" }, section = { count = 1 } })
|
||||
feature:update_config(false, { names = { "a" }, ["section.count"] = 2 })
|
||||
Suite.eq(#env.events, 2, "unchanged values raise nothing")
|
||||
Suite.eq(env.Feature.events.on_config_updated, assert(env.events[1]).name, "the event is on_config_updated")
|
||||
|
||||
local seen = updates(env)
|
||||
table.sort(seen, function(a, b) return a[2] < b[2] end)
|
||||
Suite.eq(seen, {
|
||||
{ "test", "enabled", true, false },
|
||||
{ "test", "section.count", 1, 2 },
|
||||
}, "with the path, old value, and new value")
|
||||
end)
|
||||
|
||||
Suite.test("receive_update() ignores values which can not be overridden", function(env)
|
||||
Suite.test("update_config() ignores and logs values which can not be overridden", function(env)
|
||||
local function check() return true end
|
||||
local config = env.Features.config("test", { flag = true, check = check, icon = nil })
|
||||
env.Features.receive_update{
|
||||
local feature = env.Feature.register("test", { flag = true, check = check, icon = nil })
|
||||
feature:update_config(true, { flag = "yes", check = false, icon = "x" })
|
||||
Suite.eq(feature.config, { enabled = true, flag = true, check = check }, "wrong types, functions, and unknown keys are skipped")
|
||||
Suite.eq(#env.logged, 3, "each one is logged")
|
||||
end)
|
||||
|
||||
Suite.test("receive_update() updates known features", function(env)
|
||||
local feature = env.Feature.register("test", { flag = true })
|
||||
env.Feature.receive_update{
|
||||
{ name = "test", enabled = true, values = { flag = false } },
|
||||
{ name = "unknown", enabled = false, values = {} },
|
||||
{ name = "test", enabled = true, values = { flag = "yes", check = false, icon = "x", extra = 1 } },
|
||||
}
|
||||
Suite.eq(config, { enabled = true, flag = true, check = check }, "wrong types, functions, and unknown keys are skipped")
|
||||
Suite.eq(feature.config.flag, false, "the known feature changed")
|
||||
Suite.eq(env.storage.unknown, nil, "unknown features are not stored")
|
||||
end)
|
||||
|
||||
Suite.test("on_load applies the overrides from storage", function(env)
|
||||
local config = env.Features.config("test", { flag = true })
|
||||
Suite.test("on_load applies the overrides from storage without raising events", function(env)
|
||||
local feature = env.Feature.register("test", { flag = true })
|
||||
env.on_load{ test = { enabled = false, flag = false } }
|
||||
Suite.eq(config, { enabled = false, flag = false }, "the saved overrides apply")
|
||||
Suite.eq(feature.config, { enabled = false, flag = false }, "the saved overrides apply")
|
||||
Suite.empty(env.events, "nothing is raised")
|
||||
|
||||
env.Features.receive_update{ { name = "test", enabled = true, values = {} } }
|
||||
feature:update_config(true, {})
|
||||
Suite.eq(env.storage, { test = { enabled = true } }, "later updates go to the loaded storage table")
|
||||
end)
|
||||
|
||||
Suite.test("guard() skips handlers while the feature is disabled", function(env)
|
||||
local config = env.Features.config("test")
|
||||
local feature = env.Feature.register("test")
|
||||
local calls = {}
|
||||
local lib = env.Features.guard(config, {
|
||||
local lib = feature:guard{
|
||||
events = { on_event = function(event) calls[#calls + 1] = event end },
|
||||
on_nth_tick = { [60] = function(event) calls[#calls + 1] = event end },
|
||||
on_init = function() end,
|
||||
})
|
||||
}
|
||||
|
||||
lib.events.on_event("a")
|
||||
lib.on_nth_tick[60]("b")
|
||||
env.Features.receive_update{ { name = "test", enabled = false, values = {} } }
|
||||
feature:update_config(false, {})
|
||||
lib.events.on_event("c")
|
||||
lib.on_nth_tick[60]("d")
|
||||
Suite.eq(calls, { "a", "b" }, "only called while enabled")
|
||||
end)
|
||||
|
||||
Suite.test("guard() declares features by name", function(env)
|
||||
local calls = 0
|
||||
local lib = env.Features.guard("test", { events = { on_event = function() calls = calls + 1 end } })
|
||||
env.Features.receive_update{ { name = "test", enabled = false, values = {} } }
|
||||
lib.events.on_event()
|
||||
Suite.eq(calls, 0, "the name is declared without config")
|
||||
Suite.eq(env.Features.is_enabled("test"), false, "and can be looked up")
|
||||
end)
|
||||
|
||||
Suite.test("receive_update() logs invalid and unknown overrides", function(env)
|
||||
local config = env.Features.config("test", { flag = true })
|
||||
env.Features.receive_update{ { name = "test", enabled = true, values = { flag = 1, extra = 2 } } }
|
||||
Suite.eq(config.flag, true, "the default is used")
|
||||
Suite.eq(#env.logged, 2, "both are logged")
|
||||
end)
|
||||
|
||||
Suite.test("config() addresses nested values by path", function(env)
|
||||
Suite.test("register() addresses nested values by path", function(env)
|
||||
local offset = { 1, 2 }
|
||||
local config = env.Features.config("test", { section = { flag = true, count = 1 }, offset = offset })
|
||||
env.Features.receive_update{ { name = "test", enabled = true, values = { ["section.flag"] = false, offset = { "x" } } } }
|
||||
Suite.eq(config.section, { flag = false, count = 1 }, "the nested value changes in place")
|
||||
Suite.eq(config.offset, offset, "lists of other types can not be overridden")
|
||||
local feature = env.Feature.register("test", { section = { flag = true, count = 1 }, offset = offset })
|
||||
feature:update_config(true, { ["section.flag"] = false, offset = { "x" } })
|
||||
Suite.eq(feature.config.section, { flag = false, count = 1 }, "the nested value changes in place")
|
||||
Suite.eq(feature.config.offset, offset, "lists of other types can not be overridden")
|
||||
|
||||
env.Features.receive_update{ { name = "test", enabled = true, values = {} } }
|
||||
Suite.eq(config.section.flag, true, "and goes back to its default")
|
||||
feature:update_config(true, {})
|
||||
Suite.eq(feature.config.section.flag, true, "and goes back to its default")
|
||||
end)
|
||||
|
||||
Suite.test("config() allows lists of strings to be overridden", function(env)
|
||||
local config = env.Features.config("test", { names = { "a", "b" }, empty = {} })
|
||||
env.Features.receive_update{ { name = "test", enabled = true, values = { names = { "c" }, empty = { "d" } } } }
|
||||
Suite.eq(config.names, { "c" }, "a list is replaced")
|
||||
Suite.eq(config.empty, { "d" }, "an empty default is a list")
|
||||
env.Features.receive_update{ { name = "test", enabled = true, values = { names = { 1 } } } }
|
||||
Suite.eq(config.names, { "a", "b" }, "a list with other types is invalid")
|
||||
Suite.test("register() allows lists of strings to be overridden", function(env)
|
||||
local feature = env.Feature.register("test", { names = { "a", "b" }, empty = {} })
|
||||
feature:update_config(true, { names = { "c" }, empty = { "d" } })
|
||||
Suite.eq(feature.config.names, { "c" }, "a list is replaced")
|
||||
Suite.eq(feature.config.empty, { "d" }, "an empty default is a list")
|
||||
feature:update_config(true, { names = { 1 } })
|
||||
Suite.eq(feature.config.names, { "a", "b" }, "a list with other types is invalid")
|
||||
end)
|
||||
|
||||
Suite.test("optional() declares a value without a default", function(env)
|
||||
local config = env.Features.config("test", { role_id = env.Features.optional("number") })
|
||||
Suite.eq(config.role_id, nil, "nil until overridden")
|
||||
env.Features.receive_update{ { name = "test", enabled = true, values = { role_id = 3 } } }
|
||||
Suite.eq(config.role_id, 3, "overridden with the given type")
|
||||
env.Features.receive_update{ { name = "test", enabled = true, values = {} } }
|
||||
Suite.eq(config.role_id, nil, "and back to nil")
|
||||
local feature = env.Feature.register("test", { role_id = env.Feature.optional("number") })
|
||||
Suite.eq(feature.config.role_id, nil, "nil until overridden")
|
||||
feature:update_config(true, { role_id = 3 })
|
||||
Suite.eq(feature.config.role_id, 3, "overridden with the given type")
|
||||
feature:update_config(true, {})
|
||||
Suite.eq(feature.config.role_id, nil, "and back to nil")
|
||||
end)
|
||||
|
||||
Suite.test("on_apply() is called now and after every change", function(env)
|
||||
local config = env.Features.config("test", { names = { "a" } })
|
||||
local seen = {}
|
||||
env.Features.on_apply("test", function(applied) seen[#seen + 1] = applied.names[1] end)
|
||||
env.Features.receive_update{ { name = "test", enabled = true, values = { names = { "b" } } } }
|
||||
env.on_load{ test = { enabled = true, names = { "c" } } }
|
||||
Suite.eq(seen, { "a", "b", "c" }, "on register, update, and load")
|
||||
Suite.eq(config.names, { "c" }, "after the config is applied")
|
||||
Suite.test("to_set() is cached until the list is replaced", function(env)
|
||||
local feature = env.Feature.register("test", { names = { "a" } })
|
||||
local set = env.Feature.to_set(feature.config.names)
|
||||
Suite.eq(set, { a = true }, "a set of the items")
|
||||
Suite.eq(env.Feature.to_set(feature.config.names) == set, true, "the same set while the list is unchanged")
|
||||
env.on_load{ test = { enabled = true, names = { "b" } } }
|
||||
Suite.eq(env.Feature.to_set(feature.config.names), { b = true }, "a new set after load replaces the list")
|
||||
end)
|
||||
|
||||
Suite.test("config files declare their feature", function(env)
|
||||
local config = env.load_config("popups")
|
||||
env.Features.receive_update{ { name = "popups", enabled = true, values = { show_player_damage = false } } }
|
||||
Suite.eq(config.show_player_damage, false, "popups can be changed")
|
||||
Suite.eq(config.show_player_health, true, "other values keep their default")
|
||||
Suite.test("config files register their feature", function(env)
|
||||
local feature = env.load_config("popups")
|
||||
feature:update_config(true, { show_player_damage = false })
|
||||
Suite.eq(feature.config.show_player_damage, false, "popups can be changed")
|
||||
Suite.eq(feature.config.show_player_health, true, "other values keep their default")
|
||||
end)
|
||||
|
||||
return Suite.run()
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { Button, Checkbox, Col, Divider, Input, InputNumber, Row, Select, Space, Switch } from "antd";
|
||||
import { Button, Checkbox, Col, ConfigProvider, Divider, Input, InputNumber, Row, Select, Space, Switch } from "antd";
|
||||
|
||||
import { ControlContext, PageHeader, PageLayout, notifyErrorHandler, useAccount, useRoles } from "@clusterio/web_ui";
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { ControlContext, PageHeader, PageLayout, notifyErrorHandler, useAccount } from "@clusterio/web_ui";
|
||||
|
||||
import { Feature, FeatureField, FeatureValue, features, isDefaultValue } from "../../features.js";
|
||||
import { FeatureRecord, FeatureUpdateRequest } from "../../messages.js";
|
||||
@@ -70,24 +71,30 @@ function searchFields(feature: Feature, query: string) {
|
||||
));
|
||||
}
|
||||
|
||||
function RoleSelect(props: { value: FeatureValue, disabled: boolean, onChange: (value: FeatureValue) => void }) {
|
||||
const [roles, synced] = useRoles();
|
||||
return <Select
|
||||
showSearch
|
||||
allowClear
|
||||
placeholder="No role"
|
||||
optionFilterProp="label"
|
||||
style={{ minWidth: 200 }}
|
||||
value={props.value ?? undefined}
|
||||
onChange={value => props.onChange(value ?? null)}
|
||||
options={[...roles.values()].map(role => ({ label: role.name, value: role.id }))}
|
||||
disabled={props.disabled || !synced}
|
||||
loading={!synced}
|
||||
/>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the input for a setting the same way the config pages do, input components registered
|
||||
* with the web interface take priority so plugins can provide inputs for other types
|
||||
*/
|
||||
function FieldInput(props: { field: FeatureField, value: FeatureValue, disabled: boolean, onChange: (value: FeatureValue) => void }) {
|
||||
const { field, value, disabled, onChange } = props;
|
||||
const control = useContext(ControlContext);
|
||||
const CustomInput = field.inputComponent ? control.inputComponents.get(field.inputComponent) : undefined;
|
||||
if (CustomInput) {
|
||||
const fieldDefinition = {
|
||||
type: field.type === "string_list" ? "object" : field.type,
|
||||
title: field.title,
|
||||
description: field.description,
|
||||
optional: field.optional,
|
||||
inputComponent: field.inputComponent,
|
||||
} as lib.FieldDefinition;
|
||||
return <CustomInput
|
||||
fieldDefinition={fieldDefinition}
|
||||
value={value as Exclude<FeatureValue, string[]>}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
/>;
|
||||
}
|
||||
|
||||
switch (field.type) {
|
||||
case "number":
|
||||
return <InputNumber
|
||||
@@ -95,10 +102,21 @@ function FieldInput(props: { field: FeatureField, value: FeatureValue, disabled:
|
||||
min={field.min}
|
||||
addonAfter={field.unit}
|
||||
disabled={disabled}
|
||||
onChange={next => onChange(next ?? field.default)}
|
||||
onChange={next => onChange(next ?? (field.optional ? null : field.default))}
|
||||
/>;
|
||||
case "string":
|
||||
return <Input value={value as string} disabled={disabled} onChange={e => onChange(e.target.value)} style={{ maxWidth: 400 }} />;
|
||||
if (field.enum) {
|
||||
return <Select
|
||||
showSearch
|
||||
style={{ minWidth: 175 }}
|
||||
value={value as string}
|
||||
options={field.enum.map(option => ({ label: option, value: option }))}
|
||||
allowClear={field.optional}
|
||||
disabled={disabled}
|
||||
onChange={next => onChange(next ?? null)}
|
||||
/>;
|
||||
}
|
||||
return <Input value={value as string} disabled={disabled} onChange={e => onChange(e.target.value)} style={{ width: 300 }} />;
|
||||
case "string_list":
|
||||
return <Select
|
||||
mode="tags"
|
||||
@@ -108,8 +126,6 @@ function FieldInput(props: { field: FeatureField, value: FeatureValue, disabled:
|
||||
open={false}
|
||||
style={{ width: "100%", maxWidth: 600 }}
|
||||
/>;
|
||||
case "role":
|
||||
return <RoleSelect value={value} disabled={disabled} onChange={onChange} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -160,7 +176,7 @@ function FeatureSection(props: {
|
||||
borderRadius: 4,
|
||||
background: state.enabled !== stored.enabled ? modifiedColor : undefined,
|
||||
}}>
|
||||
<Switch checked={state.enabled} disabled={!canEdit} onChange={props.onEnabled} />
|
||||
<Switch size="default" checked={state.enabled} disabled={!canEdit} onChange={props.onEnabled} />
|
||||
</div>
|
||||
<div style={{ flexGrow: 1 }}>
|
||||
<strong>{feature.title}</strong>
|
||||
@@ -301,23 +317,26 @@ export default function FeaturesPage() {
|
||||
</div>
|
||||
</Col>
|
||||
<Col flex="auto" style={{ minWidth: 0 }}>
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
{shown.map(({ feature, fields }) => <FeatureSection
|
||||
key={feature.name}
|
||||
feature={feature}
|
||||
fields={fields}
|
||||
state={current(feature.name)}
|
||||
stored={stored[feature.name]}
|
||||
canEdit={canEdit}
|
||||
onEnabled={enabled => change(feature.name, entry => { entry.enabled = enabled; })}
|
||||
onValue={(key, value) => change(feature.name, entry => { entry.values[key] = value; })}
|
||||
onReset={() => change(feature.name, entry => {
|
||||
for (const field of feature.fields) {
|
||||
entry.values[field.name] = field.default;
|
||||
}
|
||||
})}
|
||||
/>)}
|
||||
</Space>
|
||||
{/* Small inputs match the line height of the text, same as the config pages */}
|
||||
<ConfigProvider componentSize="small">
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
{shown.map(({ feature, fields }) => <FeatureSection
|
||||
key={feature.name}
|
||||
feature={feature}
|
||||
fields={fields}
|
||||
state={current(feature.name)}
|
||||
stored={stored[feature.name]}
|
||||
canEdit={canEdit}
|
||||
onEnabled={enabled => change(feature.name, entry => { entry.enabled = enabled; })}
|
||||
onValue={(key, value) => change(feature.name, entry => { entry.values[key] = value; })}
|
||||
onReset={() => change(feature.name, entry => {
|
||||
for (const field of feature.fields) {
|
||||
entry.values[field.name] = field.default;
|
||||
}
|
||||
})}
|
||||
/>)}
|
||||
</Space>
|
||||
</ConfigProvider>
|
||||
</Col>
|
||||
</Row>
|
||||
</PageLayout>;
|
||||
|
||||
Reference in new issue
Block a user