Merge branch 'explosivegaming:main' into translate

This commit is contained in:
phidias authored and GitHub committed 2026-10-05 14:33:51 +09:00
commit b0b5bf223e
104 files changed
+4673 -2819

No files matched your search

+24 -24
View File
@@ -16,10 +16,6 @@ export interface PendingRestart {
}
export class AutoRestart {
controller: Controller;
logger: lib.Logger;
name: string;
/** When each host, and the cluster as a whole, last became empty of players. Absent while players are online. */
idleSince = new Map<number | "cluster", number>();
/** Hosts that were told to restart and are waiting for their instances to come back */
@@ -31,32 +27,36 @@ export class AutoRestart {
controllerRestarting = false;
private checkInterval?: ReturnType<typeof setInterval>;
constructor(context: ControllerPluginContext) {
this.controller = context.controller;
this.logger = context.logger;
this.name = context.plugin.name;
}
private constructor(
public controller: Controller,
public logger: lib.Logger,
public name: string,
) {}
/** Attach to the controller hooks and start checking for restarts. */
start() {
const hooks = this.controller.hooks;
hooks.shutdown.attach(this.name, () => this.stop());
hooks.playerEvent.attach(this.name, (instance, event) => this.onPlayerEvent(instance, event));
hooks.instanceStatusChanged.attach(this.name, (instance, prev) => this.onInstanceStatusChanged(instance, prev));
hooks.hostConnectionEvent.attach(this.name, (connection, event) => this.onHostConnectionEvent(connection, event));
/** Create the plugin, attach to the controller hooks and start checking for restarts. */
static async fromContext(context: ControllerPluginContext) {
const controller = context.controller;
const plugin = new AutoRestart(controller, context.logger, context.plugin.name);
if (this.controller.config.get("controller.system_metrics_interval") <= 0) {
this.logger.warn(
const hooks = controller.hooks;
hooks.shutdown.attach(plugin.name, () => plugin.stop());
hooks.playerEvent.attach(plugin.name, (instance, event) => plugin.onPlayerEvent(instance, event));
hooks.instanceStatusChanged.attach(plugin.name, (instance, prev) => plugin.onInstanceStatusChanged(instance, prev));
hooks.hostConnectionEvent.attach(plugin.name, (connection, event) => plugin.onHostConnectionEvent(connection, event));
if (controller.config.get("controller.system_metrics_interval") <= 0) {
plugin.logger.warn(
"controller.system_metrics_interval is 0, so the controller never learns which hosts need a restart"
);
}
this.refreshIdle();
this.checkInterval = setInterval(() => {
this.check().catch((err: any) => {
this.logger.error(`Unexpected error checking for restarts:\n${err.stack ?? err.message}`);
plugin.refreshIdle();
plugin.checkInterval = setInterval(() => {
plugin.check().catch((err: any) => {
plugin.logger.error(`Unexpected error checking for restarts:\n${err.stack ?? err.message}`);
});
}, CHECK_INTERVAL_MS);
this.checkInterval.unref();
plugin.checkInterval.unref();
return plugin;
}
stop() {
@@ -291,5 +291,5 @@ export class AutoRestart {
}
export default async function (context: ControllerPluginContext) {
new AutoRestart(context).start();
await AutoRestart.fromContext(context);
}
+2 -3
View File
@@ -35,8 +35,7 @@ async function startPlugin(t2, { config = {}, canRestart = false } = {}) {
sent.push({ address, message });
};
const plugin = new AutoRestart({ controller, logger, plugin: pluginDeclaration, metrics: undefined });
plugin.start();
const plugin = await AutoRestart.fromContext({ controller, logger, plugin: pluginDeclaration, metrics: undefined });
t2.teardown(() => plugin.stop());
return { plugin, controller, sent, logs };
}
@@ -344,7 +343,7 @@ t.test("class AutoRestart", t2 => {
t3.ok(logs.some(([level, message]) => level === "warn" && message.includes("--can-restart")));
});
t2.test(".start() warns when system metrics are disabled", async t3 => {
t2.test(".fromContext() warns when system metrics are disabled", async t3 => {
const { logs } = await startPlugin(t3, { config: { "controller.system_metrics_interval": 0 } });
t3.ok(logs.some(([level, message]) => level === "warn" && message.includes("system_metrics_interval")));
});
+45 -44
View File
@@ -6,81 +6,82 @@ import * as path from "node:path";
const loaded = new WeakMap<Controller, ControllerPlugin>();
export class ControllerPlugin {
controller: Controller;
logger: lib.Logger;
name: string;
groups!: lib.SubscribableDatastore<messages.GroupRecord>;
roleMappings!: lib.SubscribableDatastore<messages.RoleMappingRecord>;
manualAssignments!: lib.SubscribableDatastore<messages.AssignmentRecord>;
resolvedAssignments!: lib.SubscribableDatastore<messages.AssignmentRecord>;
resolvedAssignments = new lib.SubscribableDatastore<messages.AssignmentRecord>();
/** The plugin loaded on a controller, for the exp_scenario seed. */
static get(controller: Controller) {
return loaded.get(controller);
}
constructor(context: ControllerPluginContext) {
this.controller = context.controller;
this.logger = context.logger;
this.name = context.plugin.name;
}
private constructor(
public controller: Controller,
public logger: lib.Logger,
public name: string,
public groups: lib.SubscribableDatastore<messages.GroupRecord>,
public roleMappings: lib.SubscribableDatastore<messages.RoleMappingRecord>,
public manualAssignments: lib.SubscribableDatastore<messages.AssignmentRecord>,
) {}
async init() {
loaded.set(this.controller, this);
const databaseDirectory = this.controller.config.get("controller.database_directory");
static async fromContext(context: ControllerPluginContext) {
const controller = context.controller;
const databaseDirectory = controller.config.get("controller.database_directory");
this.groups = new lib.SubscribableDatastore(
const groups = new lib.SubscribableDatastore(
...await new lib.JsonIdDatastoreProvider(
path.join(databaseDirectory, "exp_groups", "groups.json"),
messages.GroupRecord.fromJSON.bind(messages.GroupRecord),
).bootstrap()
);
this.roleMappings = new lib.SubscribableDatastore(
const roleMappings = new lib.SubscribableDatastore(
...await new lib.JsonIdDatastoreProvider(
path.join(databaseDirectory, "exp_groups", "role_mappings.json"),
messages.RoleMappingRecord.fromJSON.bind(messages.RoleMappingRecord),
).bootstrap()
);
this.manualAssignments = new lib.SubscribableDatastore(
const manualAssignments = new lib.SubscribableDatastore(
...await new lib.JsonIdDatastoreProvider(
path.join(databaseDirectory, "exp_groups", "assignments.json"),
messages.AssignmentRecord.fromJSON.bind(messages.AssignmentRecord),
).bootstrap()
);
this.resolvedAssignments = new lib.SubscribableDatastore();
const plugin = new ControllerPlugin(
controller, context.logger, context.plugin.name, groups, roleMappings, manualAssignments,
);
loaded.set(controller, plugin);
this.controller.subscriptions.handle(messages.GroupUpdatedEvent, this.handleGroupSubscription.bind(this));
this.controller.subscriptions.handle(messages.RoleMappingUpdatedEvent, this.handleRoleMappingSubscription.bind(this));
this.controller.subscriptions.handle(messages.ManualAssignmentUpdatedEvent, this.handleManualAssignmentSubscription.bind(this));
this.controller.subscriptions.handle(messages.ResolvedAssignmentUpdatedEvent, this.handleResolvedAssignmentSubscription.bind(this));
controller.subscriptions.handle(messages.GroupUpdatedEvent, plugin.handleGroupSubscription.bind(plugin));
controller.subscriptions.handle(messages.RoleMappingUpdatedEvent, plugin.handleRoleMappingSubscription.bind(plugin));
controller.subscriptions.handle(messages.ManualAssignmentUpdatedEvent, plugin.handleManualAssignmentSubscription.bind(plugin));
controller.subscriptions.handle(messages.ResolvedAssignmentUpdatedEvent, plugin.handleResolvedAssignmentSubscription.bind(plugin));
this.groups.on("update", this.groupsUpdated.bind(this));
this.roleMappings.on("update", this.roleMappingsUpdated.bind(this));
this.manualAssignments.on("update", this.manualAssignmentsUpdated.bind(this));
this.resolvedAssignments.on("update", this.resolvedAssignmentsUpdated.bind(this));
groups.on("update", plugin.groupsUpdated.bind(plugin));
roleMappings.on("update", plugin.roleMappingsUpdated.bind(plugin));
manualAssignments.on("update", plugin.manualAssignmentsUpdated.bind(plugin));
plugin.resolvedAssignments.on("update", plugin.resolvedAssignmentsUpdated.bind(plugin));
this.controller.handle(messages.GroupCreateRequest, this.handleGroupCreateRequest.bind(this));
this.controller.handle(messages.GroupUpdateRequest, this.handleGroupUpdateRequest.bind(this));
this.controller.handle(messages.GroupDeleteRequest, this.handleGroupDeleteRequest.bind(this));
this.controller.handle(messages.GroupGetRequest, this.handleGroupGetRequest.bind(this));
this.controller.handle(messages.GroupListRequest, this.handleGroupListRequest.bind(this));
controller.handle(messages.GroupCreateRequest, plugin.handleGroupCreateRequest.bind(plugin));
controller.handle(messages.GroupUpdateRequest, plugin.handleGroupUpdateRequest.bind(plugin));
controller.handle(messages.GroupDeleteRequest, plugin.handleGroupDeleteRequest.bind(plugin));
controller.handle(messages.GroupGetRequest, plugin.handleGroupGetRequest.bind(plugin));
controller.handle(messages.GroupListRequest, plugin.handleGroupListRequest.bind(plugin));
this.controller.handle(messages.AssignmentCreateRequest, this.handleAssignmentCreateRequest.bind(this));
this.controller.handle(messages.AssignmentUpdateRequest, this.handleAssignmentUpdateRequest.bind(this));
this.controller.handle(messages.AssignmentDeleteRequest, this.handleAssignmentDeleteRequest.bind(this));
this.controller.handle(messages.AssignmentGetRequest, this.handleAssignmentGetRequest.bind(this));
this.controller.handle(messages.AssignmentListRequest, this.handleAssignmentListRequest.bind(this));
controller.handle(messages.AssignmentCreateRequest, plugin.handleAssignmentCreateRequest.bind(plugin));
controller.handle(messages.AssignmentUpdateRequest, plugin.handleAssignmentUpdateRequest.bind(plugin));
controller.handle(messages.AssignmentDeleteRequest, plugin.handleAssignmentDeleteRequest.bind(plugin));
controller.handle(messages.AssignmentGetRequest, plugin.handleAssignmentGetRequest.bind(plugin));
controller.handle(messages.AssignmentListRequest, plugin.handleAssignmentListRequest.bind(plugin));
this.controller.handle(messages.RoleMappingCreateRequest, this.handleRoleMappingCreateRequest.bind(this));
this.controller.handle(messages.RoleMappingUpdateRequest, this.handleRoleMappingUpdateRequest.bind(this));
this.controller.handle(messages.RoleMappingDeleteRequest, this.handleRoleMappingDeleteRequest.bind(this));
this.controller.handle(messages.RoleMappingGetRequest, this.handleRoleMappingGetRequest.bind(this));
this.controller.handle(messages.RoleMappingListRequest, this.handleRoleMappingListRequest.bind(this));
controller.handle(messages.RoleMappingCreateRequest, plugin.handleRoleMappingCreateRequest.bind(plugin));
controller.handle(messages.RoleMappingUpdateRequest, plugin.handleRoleMappingUpdateRequest.bind(plugin));
controller.handle(messages.RoleMappingDeleteRequest, plugin.handleRoleMappingDeleteRequest.bind(plugin));
controller.handle(messages.RoleMappingGetRequest, plugin.handleRoleMappingGetRequest.bind(plugin));
controller.handle(messages.RoleMappingListRequest, plugin.handleRoleMappingListRequest.bind(plugin));
this.controller.hooks.shutdown.attach(this.name, this.onShutdown.bind(this));
controller.hooks.shutdown.attach(plugin.name, plugin.onShutdown.bind(plugin));
return plugin;
}
async onShutdown() {
@@ -453,5 +454,5 @@ export class ControllerPlugin {
}
export default async function (context: ControllerPluginContext) {
await new ControllerPlugin(context).init();
await ControllerPlugin.fromContext(context);
}
+20 -19
View File
@@ -18,9 +18,6 @@ export type IpcPlayerAssignments = {
};
export class InstancePlugin {
instance: Instance;
logger: lib.Logger;
name: string;
// Once only, don't send permissions for these groups
// This is used for groups created on this instance that only need the controller generated id
skipSendingPermissions = new Set<string>();
@@ -29,23 +26,27 @@ export class InstancePlugin {
// Track known online players so that we only apply assignment updates for them
onlinePlayers = new Set<string>();
constructor(context: InstancePluginContext) {
this.instance = context.instance;
this.logger = context.logger;
this.name = context.plugin.name;
}
private constructor(
public instance: Instance,
public logger: lib.Logger,
public name: string,
) {}
async init() {
this.instance.handle(messages.GroupUpdatedEvent, this.handleGroupUpdatedEvent.bind(this));
this.instance.handle(messages.ResolvedAssignmentUpdatedEvent, this.handleResolvedAssignmentUpdatedEvent.bind(this));
this.instance.server.handle(`exp_group:group_updated`, this.handleGroupUpdatedIPC.bind(this))
this.instance.server.handle(`exp_group:group_deleted`, this.handleGroupDeletedIPC.bind(this))
this.instance.server.handle(`exp_group:player_assignments`, this.handlePlayerAssignmentsIPC.bind(this))
static async fromContext(context: InstancePluginContext) {
const instance = context.instance;
const plugin = new InstancePlugin(instance, context.logger, context.plugin.name);
const hooks = this.instance.hooks;
hooks.instanceConfigFieldChanged.attach(this.name, this.onInstanceConfigFieldChanged.bind(this));
hooks.start.attach(this.name, this.onStart.bind(this));
hooks.playerEvent.attach(this.name, this.onPlayerEvent.bind(this));
instance.handle(messages.GroupUpdatedEvent, plugin.handleGroupUpdatedEvent.bind(plugin));
instance.handle(messages.ResolvedAssignmentUpdatedEvent, plugin.handleResolvedAssignmentUpdatedEvent.bind(plugin));
instance.server.handle(`exp_group:group_updated`, plugin.handleGroupUpdatedIPC.bind(plugin))
instance.server.handle(`exp_group:group_deleted`, plugin.handleGroupDeletedIPC.bind(plugin))
instance.server.handle(`exp_group:player_assignments`, plugin.handlePlayerAssignmentsIPC.bind(plugin))
const hooks = instance.hooks;
hooks.instanceConfigFieldChanged.attach(plugin.name, plugin.onInstanceConfigFieldChanged.bind(plugin));
hooks.start.attach(plugin.name, plugin.onStart.bind(plugin));
hooks.playerEvent.attach(plugin.name, plugin.onPlayerEvent.bind(plugin));
return plugin;
}
async onInstanceConfigFieldChanged(field: string, curr: unknown, prev: unknown) {
@@ -185,5 +186,5 @@ export class InstancePlugin {
}
export default async function (context: InstancePluginContext) {
await new InstancePlugin(context).init();
await InstancePlugin.fromContext(context);
}
-14
View File
@@ -1,14 +0,0 @@
local Roles = require("modules/exp_roles")
return {
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
afk_time = 3600 * 10, --- @setting afk_time The time in ticks that must pass for a player to be considered afk
kick_time = 3600 * 30, --- @setting kick_time The time in ticks that must pass without any active players for all players to be kicked
trust_time = 3600 * 60 * 10, --- @setting trust_time The time in ticks that a player must be online for to count as trusted
update_time = 3600 * 30, --- @setting update_time How often in ticks the script checks for active players
custom_active_check = function(player)
local veteran = Roles.get_role_by_name("Veteran")
return veteran ~= nil and not Roles.get_player_highest_role(player):is_lower_than(veteran)
end,
}
-105
View File
@@ -1,105 +0,0 @@
--- Lists all bonuses which can be used, name followed by min max
-- @config Bonuses
return {
--[[
Base point is equal to the amount of standard value in each parameter.
CMMS CRS CCS CISB CHB CRDB PBR
STD 20 60 24 10 12 8 40
= 174
MAX 40 120 48 20 24 16 80
= 348
]]
points = {
base = 174,
increase_percentage_per_role_level = 0.03,
role_name = "Member",
},
periodic_bonus_rate = 300,
player_bonus = {
{
name = "character_mining_speed_modifier",
scale = 1,
cost = 10,
max_value = 4,
value_step = 0.5,
is_percentage = true,
},
{
name = "character_running_speed_modifier",
scale = 1,
cost = 60,
max_value = 2,
value_step = 0.25,
is_percentage = true,
},
{
name = "character_crafting_speed_modifier",
scale = 1,
cost = 4,
max_value = 12,
value_step = 1,
is_percentage = true,
},
{
name = "character_inventory_slots_bonus",
scale = 10,
cost = 2,
max_value = 100,
value_step = 10,
},
{
name = "character_health_bonus",
scale = 50,
cost = 4,
max_value = 300,
value_step = 50,
},
{
name = "character_reach_distance_bonus",
scale = 1,
cost = 1,
max_value = 16,
value_step = 2,
combined_bonus = {
"character_resource_reach_distance_bonus",
"character_build_distance_bonus",
},
},
{
name = "personal_battery_recharge",
scale = 4,
cost = 40,
max_value = 8,
value_step = 1,
is_special = true,
},
},
force_bonus = {
["worker_robots_battery_modifier"] = {
max_value = 1,
value_step = 1,
scale = 1,
cost = 1,
is_percentage = false,
},
["worker_robots_storage_bonus"] = {
max_value = 1,
value_step = 1,
scale = 1,
cost = 1,
is_percentage = false,
},
["following_robots_lifetime_modifier"] = {
max_value = 1,
value_step = 1,
scale = 1,
cost = 1,
is_percentage = false,
},
},
surface_bonus = {
},
}
-26
View File
@@ -1,26 +0,0 @@
--- Config file for the compliatrons including where they spawn and what messages they show
-- @config Compilatron
local config_server_detail = require("modules.exp_legacy.config.server_detail") --- @dep config.server_detail
return {
message_cycle = 60 * 15, --- @setting message_cycle 15 seconds default, how often (in ticks) the messages will cycle
locations = {
["Spawn"] = {
spawn_position = { x = 0, y = 0 },
spawn_surface = "nauvis",
entity_name = "small-biter",
messages = {
{ "info.website", config_server_detail["website"] },
{ "info.read-readme" },
{ "info.discord", config_server_detail["discord"] },
{ "info.softmod" },
{ "info.redmew" },
{ "info.custom-commands" },
{ "info.status", config_server_detail["status"] },
{ "info.lhd" },
{ "info.github", config_server_detail["github"] },
{ "info.patreon", config_server_detail["patreon"] },
},
}
},
}
-15
View File
@@ -1,15 +0,0 @@
--- This config controls what happens when a player dies mostly about map markers and item collection;
-- allow_teleport_to_body_command and allow_collect_bodies_command can be over ridden if command_auth_runtime_disable is present;
-- if not present then the commands will not be loaded into the game
-- @config Death-Logger
return {
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,
include_time_of_death = true, --- @setting include_time_of_death weather to include the time of death on the map marker
map_icon = nil, --- @setting map_icon the icon that the map marker shows; nil means no icon; format as a SingleID
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
period_check_map_tags = 60 * 60 * 5,
}
-11
View File
@@ -1,11 +0,0 @@
--- This config controls whether actions such as deconning by players without sufficient permissions is logged or not
-- @config Deconlog
return {
decon_area = true, --- @setting decon_area whether to log when an area is being deconstructed
built_entity = true, --- @setting built_entity whether to log when an entity is built
mined_entity = true, --- @setting mined_entity whether to log when an entity is mined
fired_rocket = true, --- @setting fired_nuke whether to log when a rocket is fired
fired_explosive_rocket = true, --- @setting fired_nuke whether to log when a explosive rocket is fired
fired_nuke = true, --- @setting fired_nuke whether to log when a nuke is fired
}
@@ -1,23 +0,0 @@
--- Config file used to enable and disable different push messages for discord
-- @config Discord-Alerts
return {
show_playtime = true,
entity_protection = true,
player_bans = true,
player_mutes = true,
player_kicks = true,
player_promotes = false,
player_jail = true,
["config"] = true,
["purge"] = true,
["c"] = true,
["command"] = true,
["silent-command"] = true,
["measured-command"] = true,
["banlist"] = true,
["permissions"] = true,
["editor"] = true,
["cheat"] = true,
["open"] = false,
}
-7
View File
@@ -1,7 +0,0 @@
return {
modules = {
["forcestats"] = true,
["logistorage"] = false,
["other"] = true,
},
}
-47
View File
@@ -1,47 +0,0 @@
--- This file controls what will show in each section of the rocket info gui
-- @config Rockets
return {
stats = { --- @setting stats The data that will show in the stats section
show_stats = true, --- @setting show_stats false will hide this section all together
show_first_rocket = true, --- @setting show_first_rocket false will not show when the first rocket was launched
show_last_rocket = true, --- @setting show_last_rocket false will not show when the last rocket was launched
show_fastest_rocket = true, --- @setting show_fastest_rocket false will not show the time taken for the fastest rocket
show_total_rockets = true, --- @setting show_total_rockets false will not show the total number of rockets launched
show_game_avg = true, --- @setting show_game_avg false will hide the avg across the entire map time
rolling_avg = { --- @setting rolling_avg each number will be one statistic; 5 means the avg time taken for the last 5 rockets
5, 10, 25,
},
},
milestones = { --- @setting milestones each number will be one statistic; 5 means the time that the 5th rocket was launched
show_milestones = true, --- @setting show_milestones false will hide this section all together
1,
2,
5,
10,
20,
50,
100,
200,
500,
1000,
1500,
2000,
2500,
3000,
3500,
4000,
4500,
5000,
},
progress = { --- @setting progress The data and buttons in the build progress section
show_progress = true, --- @setting show_progress false will hide this section altogether
allow_zoom_to_map = true, --- @setting allow_zoom_to_map false will disable the zoom to map feature
allow_remote_launch = true, --- @setting allow_remote_launch false removes the remote launch button for all players
remote_launch_admins_only = false, --- @setting remote_launch_admins_only true will remove the remote launch button for all non (game) admins
remote_launch_role_permission = "exp_scenario.gui.rocket_info.remote_launch", --- @setting remote_launch_role_permission value used by custom permission system to allow or disallow the button
allow_toggle_active = true, --- @setting allow_toggle_active false removes the remote toggle auto launch button for all players
toggle_active_admins_only = false, --- @setting toggle_active_admins_only true will remove the toggle auto launch button for all non (game) admins
toggle_active_role_permission = "exp_scenario.gui.rocket_info.toggle_active", --- @setting toggle_active_role_permission value used by custom permission system to allow or disallow the button
},
}
-21
View File
@@ -1,21 +0,0 @@
--- Config file for the science info gui
-- @config Science
return {
-- list of all science packs to be shown in the gui
show_eta = true, --- @setting show_eta when true the eta for research completion will be shown
color_cutoff = 0.8, --- @setting color_cutoff the amount that production can fall before the text changes color
color_flux = 0.1, --- @setting color_flux the amount of fluctuation allowed in production before the icon changes color
"automation-science-pack",
"logistic-science-pack",
"military-science-pack",
"chemical-science-pack",
"production-science-pack",
"utility-science-pack",
"space-science-pack",
"metallurgic-science-pack",
"agricultural-science-pack",
"electromagnetic-science-pack",
"cryogenic-science-pack",
"promethium-science-pack",
}
-13
View File
@@ -1,13 +0,0 @@
--- Config file for the tasks gui
-- @config Tasks
return {
-- Adding tasks
allow_add_task = "all", --- @setting allow_add_task dictates who is allowed to add new tasks; values: all, admin, exp_roles, none
exp_roles_allow_add_task = "exp_scenario.gui.task_list.add", --- @setting exp_roles_allow_add_task if exp_roles is used then this is the required permission
-- Editing tasks
allow_edit_task = "exp_roles", --- @setting allow_edit_task dictates who is allowed to edit existing tasks; values: all, admin, exp_roles, none
exp_roles_allow_edit_task = "exp_scenario.gui.task_list.edit", --- @setting exp_roles_allow_edit_task if exp_roles is used then this is the required permission
user_can_edit_own_tasks = true, --- @settings if true then the user who made the task can edit it regardless of the allow_edit_task setting
}
-52
View File
@@ -1,52 +0,0 @@
--- This file contains all the different settings for the warp system and gui
-- @config Warps
return {
-- General config
update_smoothing = 10, --- @setting update_smoothing the amount of smoothing applied to updates to the cooldown timer, higher is better, max is 60
minimum_distance = 100, --- @setting minimum_distance the minimum distance that is allowed between warps on the same force
default_icon = { type = "item", name = "discharge-defense-equipment" }, --- @setting default_icon the default icon that will be used for warps
-- Warp cooldowns
bypass_warp_cooldown = "exp_roles", --- @setting bypass_warp_cooldown dictates who the warp cooldown is applied to; values: all, admin, exp_roles, none
exp_roles_bypass_warp_cooldown = "exp_scenario.gui.warp_list.bypass_cooldown", --- @setting exp_roles_bypass_warp_cooldown if exp_roles is used then this is the required permission
cooldown_duration = 60, --- @setting cooldown_duration the duration of the warp cooldown in seconds
-- Warp proximity
bypass_warp_proximity = "exp_roles", --- @setting bypass_warp_proximity dictates who the warp proximity is applied to; values: all, admin, exp_roles, none
exp_roles_bypass_warp_proximity = "exp_scenario.gui.warp_list.bypass_proximity", --- @setting exp_roles_bypass_warp_proximity if exp_roles is used then this is the required permission
standard_proximity_radius = 4, --- @setting standard_proximity_radius the minimum distance a player is allowed to be to a warp in order to use it
spawn_proximity_radius = 20, --- @setting spawn_proximity_radius the minimum distance a player is allowed to be from they spawn point to use warps
-- Adding warps
allow_add_warp = "exp_roles", --- @setting allow_add_warp dictates who is allowed to add warps; values: all, admin, exp_roles, none
exp_roles_allow_add_warp = "exp_scenario.gui.warp_list.add", --- @setting exp_roles_allow_add_warp if exp_roles is used then this is the required permission
-- Editing warps
allow_edit_warp = "exp_roles", --- @setting allow_edit_warp dictates who is allowed to edit warps; values: all, admin, exp_roles, none
exp_roles_allow_edit_warp = "exp_scenario.gui.warp_list.edit", --- @setting exp_roles_allow_edit_warp if exp_roles is used then this is the required permission
user_can_edit_own_warps = false, --- @settings user_can_edit_own_warps if true then the user who made the warp can edit it regardless of the allow_edit_warp setting
-- Warp area generation
entities = { --- @setting entities The entities which are created for warp areas
{ "small-lamp", -4, -2 }, { "small-lamp", -2, -4 }, { "medium-electric-pole", -3, -3 }, -- Top left corner
{ "small-lamp", 3, -2 }, { "small-lamp", 1, -4 }, { "medium-electric-pole", 2, -3 }, -- Top right corner
{ "small-lamp", 3, 1 }, { "small-lamp", 1, 3 }, { "medium-electric-pole", 2, 2 }, -- Bottom right corner
{ "small-lamp", -4, 1 }, { "small-lamp", -2, 3 }, { "medium-electric-pole", -3, 2 }, -- Bottom left corner
},
tiles = { --- @setting tiles The tiles which are created for warp areas
{ "black-refined-concrete", -4, -2 }, { "black-refined-concrete", -4, -1 }, { "black-refined-concrete", -4, 0 }, { "black-refined-concrete", -4, 1 },
{ "black-refined-concrete", -3, -3 }, { "purple-refined-concrete", -3, -2 }, { "purple-refined-concrete", -3, -1 }, { "purple-refined-concrete", -3, 0 },
{ "purple-refined-concrete", -3, 1 }, { "black-refined-concrete", -3, 2 }, { "black-refined-concrete", -2, -4 }, { "purple-refined-concrete", -2, -3 },
{ "purple-refined-concrete", -2, -2 }, { "purple-refined-concrete", -2, -1 }, { "purple-refined-concrete", -2, 0 }, { "purple-refined-concrete", -2, 1 },
{ "purple-refined-concrete", -2, 2 }, { "black-refined-concrete", -2, 3 }, { "black-refined-concrete", -1, -4 }, { "purple-refined-concrete", -1, -3 },
{ "purple-refined-concrete", -1, -2 }, { "purple-refined-concrete", -1, -1 }, { "purple-refined-concrete", -1, 0 }, { "purple-refined-concrete", -1, 1 },
{ "purple-refined-concrete", -1, 2 }, { "black-refined-concrete", -1, 3 }, { "black-refined-concrete", 0, -4 }, { "purple-refined-concrete", 0, -3 },
{ "purple-refined-concrete", 0, -2 }, { "purple-refined-concrete", 0, -1 }, { "purple-refined-concrete", 0, 0 }, { "purple-refined-concrete", 0, 1 },
{ "purple-refined-concrete", 0, 2 }, { "black-refined-concrete", 0, 3 }, { "black-refined-concrete", 1, -4 }, { "purple-refined-concrete", 1, -3 },
{ "purple-refined-concrete", 1, -2 }, { "purple-refined-concrete", 1, -1 }, { "purple-refined-concrete", 1, 0 }, { "purple-refined-concrete", 1, 1 },
{ "purple-refined-concrete", 1, 2 }, { "black-refined-concrete", 1, 3 }, { "black-refined-concrete", 2, -3 }, { "purple-refined-concrete", 2, -2 },
{ "purple-refined-concrete", 2, -1 }, { "purple-refined-concrete", 2, 0 }, { "purple-refined-concrete", 2, 1 }, { "black-refined-concrete", 2, 2 },
{ "black-refined-concrete", 3, -2 }, { "black-refined-concrete", 3, -1 }, { "black-refined-concrete", 3, 0 }, { "black-refined-concrete", 3, 1 },
},
}
@@ -1,9 +0,0 @@
--- Config to control when players items are removed, this is a list of event names that will trigger inventory clear
-- @config inventory_clear
local events = defines.events
return {
events.on_player_banned,
events.on_player_kicked,
-- events.on_player_left_game
}
-6
View File
@@ -1,6 +0,0 @@
--- Settings for lawnmower
-- @config lawnmower
return {
destroy_decoratives = false,
}
-30
View File
@@ -1,30 +0,0 @@
--- Settings for logging
-- @config logging
return {
file_name = "log/logging.log",
rocket_launch_display = {
[1] = true,
[2] = true,
[5] = true,
[10] = true,
[20] = true,
[50] = true,
[100] = true,
[200] = true,
},
rocket_launch_display_rate = 500,
disconnect_reason = {
[defines.disconnect_reason.quit] = "left the game",
[defines.disconnect_reason.dropped] = "was dropped from the game",
[defines.disconnect_reason.reconnect] = "is reconnecting",
[defines.disconnect_reason.wrong_input] = "was having a wrong input",
[defines.disconnect_reason.desync_limit_reached] = "had desync limit reached",
[defines.disconnect_reason.cannot_keep_up] = "cannot keep up",
[defines.disconnect_reason.afk] = "was afk",
[defines.disconnect_reason.kicked] = "was kicked",
[defines.disconnect_reason.kicked_and_deleted] = "was kicked and deleted",
[defines.disconnect_reason.banned] = "was banned",
[defines.disconnect_reason.switching_servers] = "is switching servers",
},
}
-8
View File
@@ -1,8 +0,0 @@
--- Settings for miner
-- @config miner
return {
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
}
-86
View File
@@ -1,86 +0,0 @@
return {
module_slots_per_row = 4,
module_slot_max = 8,
copy_paste_module = true,
copy_paste_rotation = false,
machines = {
},
machine_sets = {
["base"] = {
["electric-mining-drill"] = {
["module"] = "efficiency-module",
["prod"] = true,
},
["pumpjack"] = {
["module"] = "efficiency-module",
["prod"] = true,
},
["assembling-machine-2"] = {
["module"] = "productivity-module",
["prod"] = true,
},
["assembling-machine-3"] = {
["module"] = "productivity-module-3",
["prod"] = true,
},
["electric-furnace"] = {
["module"] = "productivity-module-3",
["prod"] = true,
},
["beacon"] = {
["module"] = "speed-module-3",
["prod"] = false,
},
["oil-refinery"] = {
["module"] = "productivity-module-3",
["prod"] = true,
},
["chemical-plant"] = {
["module"] = "productivity-module-3",
["prod"] = true,
},
["centrifuge"] = {
["module"] = "productivity-module-3",
["prod"] = true,
},
["lab"] = {
["module"] = "productivity-module-3",
["prod"] = true,
},
["rocket-silo"] = {
["module"] = "productivity-module-3",
["prod"] = true,
}
},
["space-age"] = {
["big-mining-drill"] = {
["module"] = "efficiency-module",
["prod"] = true,
},
["biochamber"] = {
["module"] = "efficiency-module",
["prod"] = true,
},
["electromagnetic-plant"] = {
["module"] = "productivity-module-3",
["prod"] = true,
},
["cryogenic-plant"] = {
["module"] = "productivity-module-3",
["prod"] = true,
},
["biolab"] = {
["module"] = "productivity-module-3",
["prod"] = true,
},
["foundry"] = {
["module"] = "productivity-module-3",
["prod"] = true,
},
["recycler"] = {
["module"] = "quality-module-3",
["prod"] = true,
}
}
}
}
-30
View File
@@ -1,30 +0,0 @@
return {
inventories = {
{
inventory = defines.inventory.character_ammo,
event = defines.events.on_player_ammo_inventory_changed,
items = {
["atomic-bomb"] = true,
},
},
{
inventory = defines.inventory.character_armor,
event = defines.events.on_player_armor_inventory_changed,
items = {},
},
{
inventory = defines.inventory.character_guns,
event = defines.events.on_player_gun_inventory_changed,
items = {},
},
{
inventory = defines.inventory.character_main,
event = defines.events.on_player_main_inventory_changed,
items = {
["atomic-bomb"] = true,
},
},
},
ignore_permission = "exp_scenario.bypass.nuke_protection", -- @setting ignore_permission The permission that nukeprotect will ignore
ignore_admins = true, -- @setting ignore_admins Ignore admins, true by default. Allows usage outside of the roles module
}
@@ -1,9 +0,0 @@
--- This controls how pollution is viewed on the map
-- @config Pollution-Grading
return {
reference_point = { x = 0, y = 0 }, --- @setting reference_point where pollution is read from
max_scalar = 0.5, --- @setting max_scalar the scale between true max and max
min_scalar = 0.17, --- @setting min_scalar the scale between the lowest max and min
update_delay = 15, --- @setting update_delay time in minutes between view updates
}
@@ -1,10 +0,0 @@
--- A combination of config settings for different popup values like chat and damage
-- @config Popup-Messages
return {
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
show_player_health = true, --- @setting show_player_health weather to show player health when attacked
damage_location_variance = 0.8, --- @setting damage_location_variance how close to the eade of an entity the popups will appear
}
-17
View File
@@ -1,17 +0,0 @@
return {
ignore_admins = true, --- @setting ignore_admins If admins are ignored by the protection filter
ignore_permission = "exp_scenario.bypass.entity_protection", --- @setting ignore_permission Players with this permission will be ignored by the protection filter, leave nil to protect against every player
repeat_count = 5, --- @setting repeat_count Number of protected entities that must be removed within repeat_lifetime in order to trigger repeated removal protection
repeat_lifetime = 3600 * 20, --- @setting repeat_lifetime The length of time, in ticks, that protected removals will be remembered for
refresh_rate = 3600 * 5, --- @setting refresh_rate How often the age of protected removals are checked against repeat_lifetime
always_protected_names = { --- @setting always_protected_names Names of entities which are always protected
},
always_protected_types = { --- @setting always_protected_types Types of entities which are always protected
"boiler", "generator", "offshore-pump", "reactor", "heat-exchanger", "heat-pipe", "fusion-reactor", "fusion-generator", "power-switch", "rocket-silo",
},
always_trigger_repeat_names = { --- @setting always_trigger_repeat_names Names of entities which always trigger repeated removal protection
},
always_trigger_repeat_types = { --- @setting always_trigger_repeat_types Types of entities which always trigger repeated removal protection
"reactor", "fusion-reactor", "rocket-silo",
},
}
-8
View File
@@ -1,8 +0,0 @@
--- Config file for the repair command
-- @config Repair
return {
allow_blueprint_repair = false, --- @setting allow_blueprint_repair when true will allow blueprints (things not destroyed by biters) to be build instantly using the repair command
allow_ghost_revive = true, --- @setting allow_ghost_revive when true will allow ghosts (things destroyed by biters) to be build instantly using the repair command
allow_heal_entities = true, --- @setting allow_heal_entities when true will heal entities to full health that are within range
}
-116
View File
@@ -1,116 +0,0 @@
--- This file controls the placement/degrading of tiles as players build and walk
-- @config Scorched-Earth
return {
weakness_value = 70, --- @setting weakness_value lower value will make tiles more likely to degrade
strengths = { --- @setting strengths this decides how "strong" a tile is, bigger number means less likely to degrade
-- debug: /interface require('modules.addons.worn-paths')(player.name,true)
-- note: tiles are effected by the tiles around them, so player paths will not degrade as fast when made wider
-- note: values are relative to the tile with the highest value, recommended to keep highest tile as a "nice" number
-- note: tiles not in list will never degrade under any conditions (which is why some are omitted such as water)
["refined-concrete"] = 100,
["refined-hazard-concrete-left"] = 100,
["refined-hazard-concrete-right"] = 100,
["concrete"] = 90,
["hazard-concrete-left"] = 90,
["hazard-concrete-right"] = 90,
["stone-path"] = 80,
["red-desert-0"] = 80,
["dry-dirt"] = 50,
-- grass four (main grass tiles)
["grass-1"] = 50,
["grass-2"] = 40,
["grass-3"] = 30,
["grass-4"] = 25,
-- red three (main red tiles)
["red-desert-1"] = 40,
["red-desert-2"] = 30,
["red-desert-3"] = 25,
-- sand three (main sand tiles)
["sand-1"] = 40,
["sand-2"] = 30,
["sand-3"] = 25,
-- dirt 3 (main dirt tiles)
["dirt-1"] = 40,
["dirt-2"] = 30,
["dirt-3"] = 25,
-- last three/four (all sets of three merge here)
["dirt-4"] = 25,
["dirt-5"] = 30,
["dirt-6"] = 40,
-- ["dirt-7"]=0, -- last tile, nothing to degrade to
-- land fill chain
-- ["landfill"]=50,
-- ["water-shallow"]=90,
-- ["water-mud"]=0, -- last tile, nothing to degrade to
},
degrade_order = { --- @setting degrade_order when a tile degrades it will turn into the next tile given here
["refined-concrete"] = "concrete",
["refined-hazard-concrete-left"] = "hazard-concrete-left",
["refined-hazard-concrete-right"] = "hazard-concrete-right",
["concrete"] = "stone-path",
["hazard-concrete-left"] = "stone-path",
["hazard-concrete-right"] = "stone-path",
["stone-path"] = "dry-dirt",
["red-desert-0"] = "dry-dirt",
["dry-dirt"] = "dirt-4",
-- grass four (main grass tiles)
["grass-1"] = "grass-2",
["grass-2"] = "grass-3",
["grass-3"] = "grass-4",
["grass-4"] = "dirt-4",
-- red three (main red tiles)
["red-desert-1"] = "red-desert-2",
["red-desert-2"] = "red-desert-3",
["red-desert-3"] = "dirt-4",
-- sand three (main sand tiles)
["sand-1"] = "sand-2",
["sand-2"] = "sand-3",
["sand-3"] = "dirt-4",
-- dirt 3 (main dirt tiles)
["dirt-1"] = "dirt-2",
["dirt-2"] = "dirt-3",
["dirt-3"] = "dirt-4",
-- last three/four (all sets of three merge here)
["dirt-4"] = "dirt-5",
["dirt-5"] = "dirt-6",
["dirt-6"] = "dirt-7",
-- ["dirt-7"]=0, -- last tile, nothing to degrade to
-- land fill chain
-- ["landfill"]='grass-2', -- 'water-shallow'
-- ["water-shallow"]='water-mud',
-- ["water-mud"]=0, -- last tile, nothing to degrade to
},
entities = { --- @setting entities entities in this list will degrade the tiles under them when they are placed
["stone-furnace"] = true,
["steel-furnace"] = true,
["electric-furnace"] = true,
["assembling-machine-1"] = true,
["assembling-machine-2"] = true,
["assembling-machine-3"] = true,
["beacon"] = true,
["centrifuge"] = true,
["chemical-plant"] = true,
["oil-refinery"] = true,
["storage-tank"] = true,
["nuclear-reactor"] = true,
["steam-engine"] = true,
["steam-turbine"] = true,
["boiler"] = true,
["heat-exchanger"] = true,
["stone-wall"] = true,
["gate"] = true,
["gun-turret"] = true,
["laser-turret"] = true,
["flamethrower-turret"] = true,
["radar"] = true,
["lab"] = true,
["big-electric-pole"] = true,
["substation"] = true,
["rocket-silo"] = true,
["pumpjack"] = true,
["electric-mining-drill"] = true,
["roboport"] = true,
["accumulator"] = true,
},
}
@@ -1,11 +0,0 @@
return {
--[[
__icon__
__item_name__
__backer_name__
__direction__
__x__
__y__
]]
station_name = "[L] __icon__",
}
-180
View File
@@ -1,180 +0,0 @@
--- Settings for vlayer including the allowed items, the update frequency, and some cheats
-- @config Vlayer
return {
update_tick_storage = 60, --- @setting update_tick_storage The number of ticks between each update of the storage interfaces
update_tick_energy = 10, --- @setting update_tick_energy The number of ticks between each update of the energy and circuit interfaces
update_tick_gui = 60, --- @setting update_tick_gui The number of ticks between each update of the gui
unlimited_capacity = false, --- @setting unlimited_capacity When true the vlayer has an unlimited energy capacity, accumulators are not required
unlimited_surface_area = false, --- @setting unlimited_surface_area When true the vlayer has an unlimited surface area, landfill is not required
modded_auto_downgrade = false, --- @setting modded_auto_downgrade When true modded items will be converted into their base game equivalent, original items can not be recovered
power_on_space = false, --- @setting power_on_space When true allow on spaceship
power_on_space_research = { --- @setting power_on_space_research the research level needed to use power_on_space
name = "research-productivity",
level = 10
},
mimic_surface = "nauvis", --- @setting mimic_surface Surface name/index the vlayer will copy its settings from, use nil to use the settings below
surface = { --- @setting surface When mimic_surface is nil these settings will be used instead, see LuaSurface for details
always_day = false,
solar_power_multiplier = 1,
min_brightness = 0.15,
ticks_per_day = 25200,
daytime = 0,
dusk = 0.25,
evening = 0.45,
morning = 0.55,
dawn = 0.75,
},
interface_limit = { --- @setting interface_limit Sets the limit for the number of vlayer interfaces that can be created
energy = 1, -- >1 allows for disconnected power networks to receive power
circuit = 20, -- No caveats
storage_input = 20, -- No caveats
storage_output = 1, -- >0 allows for item teleportation (allowed_items only)
},
--- @class Vlayer.ItemProperties
--- @field starting_value number
--- @field required_area number
--- @field production number?
--- @field discharge number?
--- @field capacity number?
--- @field surface_area number?
--- @field fuel_value number?
--- @field power boolean?
--- @field modded boolean? Set when the item is registered from a modded equivalent
--- @field base_game_equivalent string?
--- @field multiplier number?
--- @type table<string, Vlayer.ItemProperties>
allowed_items = { --- @setting allowed_items List of all items allowed in vlayer storage and their properties
--[[
Allowed properties:
starting_value : The amount of the item placed into the vlayer on game start, ignores area requirements
required_area : When greater than 0 the items properties are not applied unless their is sufficient surplus surface area
production : The energy production of the item in MW, used for solar panels
discharge : The energy discharge of the item in MW, used for accumulators
capacity : The energy capacity of the item in MJ, used for accumulators
surface_area : The surface area provided by the item, used for landfill
]]
["solar-panel"] = {
starting_value = 0,
required_area = 9,
production = 0.06, -- MW
},
["accumulator"] = {
starting_value = 0,
required_area = 4,
discharge = 0.3, -- MW
capacity = 5, -- MJ
},
["landfill"] = {
starting_value = 0,
required_area = 0,
surface_area = 20, -- Tiles
},
["wood"] = {
starting_value = 0,
required_area = 0,
surface_area = 0,
fuel_value = 2, -- MJ
power = true, -- turn all wood to power to reduce trash
},
["coal"] = {
starting_value = 0,
required_area = 0,
surface_area = 0,
fuel_value = 4, -- MJ
power = false, -- turn all coal to power to reduce trash
},
["solid-fuel"] = {
starting_value = 0,
required_area = 0,
surface_area = 0,
fuel_value = 12, -- MJ
power = false, -- turn all solid fuel to power to reduce trash
},
["rocket-fuel"] = {
starting_value = 0,
required_area = 0,
surface_area = 0,
fuel_value = 100, -- MJ
power = false, -- turn all rocket fuel to power to reduce trash
}
},
modded_items = { --- @setting modded_items List of all modded items allowed in vlayer storage and their base game equivalent
["solar-panel-2"] = {
starting_value = 0,
base_game_equivalent = "solar-panel",
multiplier = 4,
},
["solar-panel-3"] = {
starting_value = 0,
base_game_equivalent = "solar-panel",
multiplier = 16,
},
["solar-panel-4"] = {
starting_value = 0,
base_game_equivalent = "solar-panel",
multiplier = 64,
},
["solar-panel-5"] = {
starting_value = 0,
base_game_equivalent = "solar-panel",
multiplier = 256,
},
["solar-panel-6"] = {
starting_value = 0,
base_game_equivalent = "solar-panel",
multiplier = 1024,
},
["solar-panel-7"] = {
starting_value = 0,
base_game_equivalent = "solar-panel",
multiplier = 4096,
},
["solar-panel-8"] = {
starting_value = 0,
base_game_equivalent = "solar-panel",
multiplier = 16384,
},
["accumulator-2"] = {
starting_value = 0,
base_game_equivalent = "accumulator",
multiplier = 4,
},
["accumulator-3"] = {
starting_value = 0,
base_game_equivalent = "accumulator",
multiplier = 16,
},
["accumulator-4"] = {
starting_value = 0,
base_game_equivalent = "accumulator",
multiplier = 64,
},
["accumulator-5"] = {
starting_value = 0,
base_game_equivalent = "accumulator",
multiplier = 256,
},
["accumulator-6"] = {
starting_value = 0,
base_game_equivalent = "accumulator",
multiplier = 1024,
},
["accumulator-7"] = {
starting_value = 0,
base_game_equivalent = "accumulator",
multiplier = 4096,
},
["accumulator-8"] = {
starting_value = 0,
base_game_equivalent = "accumulator",
multiplier = 16384,
},
}
}
+21 -21
View File
@@ -4,36 +4,36 @@ import * as messages from "./messages.js";
import * as path from "node:path";
export class ControllerPlugin {
controller: Controller;
logger: lib.Logger;
name: string;
reports!: lib.SubscribableDatastore<messages.ReportRecord>;
private constructor(
public controller: Controller,
public logger: lib.Logger,
public name: string,
public reports: lib.SubscribableDatastore<messages.ReportRecord>,
) {}
constructor(context: ControllerPluginContext) {
this.controller = context.controller;
this.logger = context.logger;
this.name = context.plugin.name;
}
static async fromContext(context: ControllerPluginContext) {
const controller = context.controller;
const databaseDirectory = controller.config.get("controller.database_directory");
async init() {
const databaseDirectory = this.controller.config.get("controller.database_directory");
this.reports = new lib.SubscribableDatastore(
const reports = new lib.SubscribableDatastore(
...await new lib.JsonIdDatastoreProvider(
path.join(databaseDirectory, "exp_reports", "reports.json"),
messages.ReportRecord.fromJSON.bind(messages.ReportRecord),
).bootstrap()
);
this.controller.subscriptions.handle(messages.ReportUpdatedEvent, this.handleReportSubscription.bind(this));
this.reports.on("update", this.reportsUpdated.bind(this));
const plugin = new ControllerPlugin(controller, context.logger, context.plugin.name, reports);
this.controller.handle(messages.ReportListRequest, this.handleReportListRequest.bind(this));
this.controller.handle(messages.ReportGetRequest, this.handleReportGetRequest.bind(this));
this.controller.handle(messages.ReportCreateRequest, this.handleReportCreateRequest.bind(this));
this.controller.handle(messages.ReportDeleteRequest, this.handleReportDeleteRequest.bind(this));
controller.subscriptions.handle(messages.ReportUpdatedEvent, plugin.handleReportSubscription.bind(plugin));
reports.on("update", plugin.reportsUpdated.bind(plugin));
this.controller.hooks.shutdown.attach(this.name, this.onShutdown.bind(this));
controller.handle(messages.ReportListRequest, plugin.handleReportListRequest.bind(plugin));
controller.handle(messages.ReportGetRequest, plugin.handleReportGetRequest.bind(plugin));
controller.handle(messages.ReportCreateRequest, plugin.handleReportCreateRequest.bind(plugin));
controller.handle(messages.ReportDeleteRequest, plugin.handleReportDeleteRequest.bind(plugin));
controller.hooks.shutdown.attach(plugin.name, plugin.onShutdown.bind(plugin));
return plugin;
}
async onShutdown() {
@@ -154,5 +154,5 @@ export class ControllerPlugin {
}
export default async function (context: ControllerPluginContext) {
await new ControllerPlugin(context).init();
await ControllerPlugin.fromContext(context);
}
+13 -13
View File
@@ -30,20 +30,20 @@ export type IpcReportDelete = {
* stopped by then, in which case the answer is dropped.
*/
export class InstancePlugin {
instance: Instance;
logger: lib.Logger;
name: string;
private constructor(
public instance: Instance,
public logger: lib.Logger,
public name: string,
) {}
constructor(context: InstancePluginContext) {
this.instance = context.instance;
this.logger = context.logger;
this.name = context.plugin.name;
}
static async fromContext(context: InstancePluginContext) {
const instance = context.instance;
const plugin = new InstancePlugin(instance, context.logger, context.plugin.name);
async init() {
this.instance.server.handle("exp_reports:create", this.handleCreateIPC.bind(this));
this.instance.server.handle("exp_reports:list", this.handleListIPC.bind(this));
this.instance.server.handle("exp_reports:delete", this.handleDeleteIPC.bind(this));
instance.server.handle("exp_reports:create", plugin.handleCreateIPC.bind(plugin));
instance.server.handle("exp_reports:list", plugin.handleListIPC.bind(plugin));
instance.server.handle("exp_reports:delete", plugin.handleDeleteIPC.bind(plugin));
return plugin;
}
async handleCreateIPC(event: IpcReportCreate) {
@@ -106,5 +106,5 @@ export class InstancePlugin {
}
export default async function (context: InstancePluginContext) {
await new InstancePlugin(context).init();
await InstancePlugin.fromContext(context);
}
+2 -3
View File
@@ -46,10 +46,9 @@ async function startPlugin(t2, { config = {} } = {}) {
broadcast(event);
};
const plugin = new ControllerPlugin({ plugin: { name: "exp_reports" }, controller, logger });
plugin.logger = { ...logger, warn: message => state.warnings.push(message) };
const pluginLogger = { ...logger, warn: message => state.warnings.push(message) };
const plugin = await ControllerPlugin.fromContext({ plugin: { name: "exp_reports" }, controller, logger: pluginLogger });
plugin.postWebhook = async (url, body) => { state.posts.push({ url, body }); };
await plugin.init();
return { plugin, controller, state };
}
+1 -2
View File
@@ -63,8 +63,7 @@ async function startPlugin(t2, { reports = [report(1, "bob", "alice"), report(2,
instance.notifyStatus("running");
state.sent.length = 0;
const plugin = new InstancePlugin({ plugin: { name: "exp_reports" }, instance, host: {}, logger });
await plugin.init();
const plugin = await InstancePlugin.fromContext({ plugin: { name: "exp_reports" }, instance, host: {}, logger });
return { plugin, instance, state };
}
+32 -32
View File
@@ -6,59 +6,59 @@ import * as path from "node:path";
const loaded = new WeakMap<Controller, ControllerPlugin>();
export class ControllerPlugin {
controller: Controller;
logger: lib.Logger;
name: string;
roleMeta!: lib.SubscribableDatastore<messages.RoleMetaRecord>;
/** The plugin loaded on a controller, for the exp_scenario seed. */
static get(controller: Controller) {
return loaded.get(controller);
}
constructor(context: ControllerPluginContext) {
this.controller = context.controller;
this.logger = context.logger;
this.name = context.plugin.name;
}
private constructor(
public controller: Controller,
public logger: lib.Logger,
public name: string,
public roleMeta: lib.SubscribableDatastore<messages.RoleMetaRecord>,
) {}
async init() {
loaded.set(this.controller, this);
const databaseDirectory = this.controller.config.get("controller.database_directory");
static async fromContext(context: ControllerPluginContext) {
const controller = context.controller;
const databaseDirectory = controller.config.get("controller.database_directory");
this.roleMeta = new lib.SubscribableDatastore(
const roleMeta = new lib.SubscribableDatastore(
...await new lib.JsonIdDatastoreProvider(
path.join(databaseDirectory, "exp_roles", "role_meta.json"),
messages.RoleMetaRecord.fromJSON.bind(messages.RoleMetaRecord),
).bootstrap()
);
const plugin = new ControllerPlugin(controller, context.logger, context.plugin.name, roleMeta);
loaded.set(controller, plugin);
// The datastore can be out of step with the roles, either because the
// plugin was installed after they were created or because it was
// uninstalled while they were deleted
this.ensureRoleMeta();
this.sweepRoleMeta();
this.applyAutoAssign();
plugin.ensureRoleMeta();
plugin.sweepRoleMeta();
plugin.applyAutoAssign();
this.controller.subscriptions.handle(messages.RoleUpdatedEvent, this.handleRoleSubscription.bind(this));
this.controller.subscriptions.handle(
messages.AssignmentUpdatedEvent, this.handleAssignmentSubscription.bind(this)
controller.subscriptions.handle(messages.RoleUpdatedEvent, plugin.handleRoleSubscription.bind(plugin));
controller.subscriptions.handle(
messages.AssignmentUpdatedEvent, plugin.handleAssignmentSubscription.bind(plugin)
);
this.roleMeta.on("update", this.roleMetaUpdated.bind(this));
this.controller.roles.on("update", this.rolesUpdated.bind(this));
this.controller.users.records.on("update", this.usersUpdated.bind(this));
roleMeta.on("update", plugin.roleMetaUpdated.bind(plugin));
controller.roles.on("update", plugin.rolesUpdated.bind(plugin));
controller.users.records.on("update", plugin.usersUpdated.bind(plugin));
this.controller.handle(messages.RoleListRequest, this.handleRoleListRequest.bind(this));
this.controller.handle(messages.RoleMetaUpdateRequest, this.handleRoleMetaUpdateRequest.bind(this));
controller.handle(messages.RoleListRequest, plugin.handleRoleListRequest.bind(plugin));
controller.handle(messages.RoleMetaUpdateRequest, plugin.handleRoleMetaUpdateRequest.bind(plugin));
this.controller.handle(messages.AssignmentListRequest, this.handleAssignmentListRequest.bind(this));
this.controller.handle(messages.AssignmentUpdateRequest, this.handleAssignmentUpdateRequest.bind(this));
controller.handle(messages.AssignmentListRequest, plugin.handleAssignmentListRequest.bind(plugin));
controller.handle(messages.AssignmentUpdateRequest, plugin.handleAssignmentUpdateRequest.bind(plugin));
const hooks = this.controller.hooks;
hooks.shutdown.attach(this.name, this.onShutdown.bind(this));
hooks.controllerConfigFieldChanged.attach(this.name, this.onControllerConfigFieldChanged.bind(this));
hooks.playerEvent.attach(this.name, this.onPlayerEvent.bind(this));
const hooks = controller.hooks;
hooks.shutdown.attach(plugin.name, plugin.onShutdown.bind(plugin));
hooks.controllerConfigFieldChanged.attach(plugin.name, plugin.onControllerConfigFieldChanged.bind(plugin));
hooks.playerEvent.attach(plugin.name, plugin.onPlayerEvent.bind(plugin));
return plugin;
}
async onShutdown() {
@@ -335,5 +335,5 @@ export class ControllerPlugin {
}
export default async function (context: ControllerPluginContext) {
await new ControllerPlugin(context).init();
await ControllerPlugin.fromContext(context);
}
+16 -16
View File
@@ -10,24 +10,24 @@ export type IpcAssignmentUpdate = {
};
export class InstancePlugin {
instance: Instance;
logger: lib.Logger;
name: string;
private constructor(
public instance: Instance,
public logger: lib.Logger,
public name: string,
) {}
constructor(context: InstancePluginContext) {
this.instance = context.instance;
this.logger = context.logger;
this.name = context.plugin.name;
}
static async fromContext(context: InstancePluginContext) {
const instance = context.instance;
const plugin = new InstancePlugin(instance, context.logger, context.plugin.name);
async init() {
this.instance.handle(messages.RoleUpdatedEvent, this.handleRoleUpdatedEvent.bind(this));
this.instance.handle(messages.AssignmentUpdatedEvent, this.handleAssignmentUpdatedEvent.bind(this));
this.instance.server.handle("exp_roles:assignment_update", this.handleAssignmentUpdateIPC.bind(this));
instance.handle(messages.RoleUpdatedEvent, plugin.handleRoleUpdatedEvent.bind(plugin));
instance.handle(messages.AssignmentUpdatedEvent, plugin.handleAssignmentUpdatedEvent.bind(plugin));
instance.server.handle("exp_roles:assignment_update", plugin.handleAssignmentUpdateIPC.bind(plugin));
const hooks = this.instance.hooks;
hooks.instanceConfigFieldChanged.attach(this.name, this.onInstanceConfigFieldChanged.bind(this));
hooks.start.attach(this.name, this.onStart.bind(this));
const hooks = instance.hooks;
hooks.instanceConfigFieldChanged.attach(plugin.name, plugin.onInstanceConfigFieldChanged.bind(plugin));
hooks.start.attach(plugin.name, plugin.onStart.bind(plugin));
return plugin;
}
get syncMode() {
@@ -122,5 +122,5 @@ export class InstancePlugin {
}
export default async function (context: InstancePluginContext) {
await new InstancePlugin(context).init();
await InstancePlugin.fromContext(context);
}
+2 -3
View File
@@ -44,8 +44,7 @@ async function startPlugin(t2, { roles = [] } = {}) {
permissionsUpdated(user);
};
const plugin = new ControllerPlugin({ plugin: { name: "exp_roles" }, controller, logger });
await plugin.init();
const plugin = await ControllerPlugin.fromContext({ plugin: { name: "exp_roles" }, controller, logger });
return { plugin, controller, state };
}
@@ -62,7 +61,7 @@ t.test("entrypoint", async t2 => {
});
t.test("class ControllerPlugin", t2 => {
t2.test(".init() and .sweepRoleMeta() manage the role properties", async t3 => {
t2.test(".fromContext() and .sweepRoleMeta() manage the role properties", async t3 => {
const { plugin } = await startPlugin(t3, { roles: [role(0, "Cluster Admin"), role(5, "Moderator")] });
t3.strictSame(plugin.roleMeta.get(0).order, 1, "the first role is ordered first");
+2 -3
View File
@@ -69,9 +69,8 @@ async function startPlugin(t2, { syncMode = "bidirectional", roles = sampleRoles
return undefined;
};
const plugin = new InstancePlugin({ plugin: { name: "exp_roles" }, instance, host: {}, logger });
plugin.logger = { ...logger, warn: message => state.warnings.push(message) };
await plugin.init();
const pluginLogger = { ...logger, warn: message => state.warnings.push(message) };
const plugin = await InstancePlugin.fromContext({ plugin: { name: "exp_roles" }, instance, host: {}, logger: pluginLogger });
return { plugin, state };
}
+83 -8
View File
@@ -1,23 +1,98 @@
import * as lib from "@clusterio/lib";
import * as path from "node:path";
import type { Controller, ControllerPluginContext } from "@clusterio/controller";
import { RoleMetaRecord } from "@expcluster/roles";
import { GroupRecord, GroupPermissions, RoleMappingRecord } from "@expcluster/permission-groups";
import { ControllerPlugin as RolesPlugin } from "@expcluster/roles/dist/node/controller.js";
import { ControllerPlugin as GroupsPlugin } from "@expcluster/permission-groups/dist/node/controller.js";
import * as messages from "./messages.js";
import { features, pruneFeatureValues, validateFeatureValues } from "./features.js";
import { SeedRole, SeedGroup, seedRoles, seedGroups, flattenSeedPermissions } from "./seed.js";
export class ControllerPlugin {
controller: Controller;
logger: lib.Logger;
private constructor(
public controller: Controller,
public logger: lib.Logger,
public name: string,
public features: lib.SubscribableDatastore<messages.FeatureRecord>,
) {}
constructor(context: ControllerPluginContext) {
this.controller = context.controller;
this.logger = context.logger;
static async fromContext(context: ControllerPluginContext) {
const controller = context.controller;
const databaseDirectory = controller.config.get("controller.database_directory");
const features = new lib.SubscribableDatastore(
...await new lib.JsonIdDatastoreProvider(
path.join(databaseDirectory, "exp_scenario", "features.json"),
messages.FeatureRecord.fromJSON.bind(messages.FeatureRecord),
).bootstrap()
);
const plugin = new ControllerPlugin(controller, context.logger, context.plugin.name, features);
plugin.reconcileFeatures();
controller.subscriptions.handle(messages.FeatureUpdatedEvent, plugin.handleFeatureSubscription.bind(plugin));
features.on("update", plugin.featuresUpdated.bind(plugin));
controller.handle(messages.SeedRequest, plugin.handleSeedRequest.bind(plugin));
controller.handle(messages.FeatureListRequest, plugin.handleFeatureListRequest.bind(plugin));
controller.handle(messages.FeatureUpdateRequest, plugin.handleFeatureUpdateRequest.bind(plugin));
controller.hooks.shutdown.attach(plugin.name, plugin.onShutdown.bind(plugin));
return plugin;
}
async init() {
this.controller.handle(messages.SeedRequest, this.handleSeedRequest.bind(this));
async onShutdown() {
await this.features.save();
}
/** Add new features, delete removed ones, and drop stored values which no longer fit a field */
reconcileFeatures() {
const declared = new Map(features.map(feature => [feature.name, feature]));
for (const record of this.features.values()) {
const feature = declared.get(record.id);
if (!feature) {
this.logger.warn(`Dropping stored config of removed feature ${record.id}`);
this.features.delete(record);
continue;
}
const { kept, dropped } = pruneFeatureValues(feature, record.values);
if (dropped.length) {
this.logger.warn(`Dropping stored values of ${record.id} which no longer match a setting: ${dropped.join(", ")}`);
this.features.set(new messages.FeatureRecord(record.id, record.enabled, kept));
}
}
const missing = features.filter(feature => !this.features.has(feature.name));
if (missing.length) {
this.features.setMany(missing.map(feature => new messages.FeatureRecord(feature.name, true)));
}
}
featuresUpdated(updates: messages.FeatureRecord[]) {
this.controller.subscriptions.broadcast(new messages.FeatureUpdatedEvent(updates));
}
async handleFeatureSubscription(request: lib.SubscriptionRequest) {
const updates = [...this.features.values()].filter(feature => feature.updatedAtMs > request.lastRequestTimeMs);
return updates.length ? new messages.FeatureUpdatedEvent(updates) : null;
}
async handleFeatureListRequest() {
return [...this.features.values()];
}
async handleFeatureUpdateRequest(request: messages.FeatureUpdateRequest) {
let values;
try {
values = validateFeatureValues(request.id, request.values);
} catch (err: any) {
throw new lib.RequestError(err.message);
}
const feature = new messages.FeatureRecord(request.id, request.enabled, values);
this.features.set(feature);
return feature;
}
/**
@@ -157,7 +232,7 @@ export class ControllerPlugin {
}
export default async function (context: ControllerPluginContext) {
await new ControllerPlugin(context).init();
await ControllerPlugin.fromContext(context);
}
function newId(datastore: { has(id: number): boolean }) {
+475
View File
@@ -0,0 +1,475 @@
/**
* The features which can be configured from the web interface. Defaults must
* match the lua side, test/features.test.js checks this.
*/
export type FeatureValue = boolean | number | string | string[] | null;
/** A setting, shaped like a clusterio config field so the config inputs can render it */
export type FeatureField = {
name: string,
title: string,
description: string,
} & (
| ScalarField<{ type: "boolean", default: boolean }>
| ScalarField<{ type: "number", default: number | null, min?: number, unit?: string }>
| ScalarField<{ type: "string", default: string | null, enum?: string[] }>
/** Feature.list or Feature.set on the lua side, both are sent as a list */
| { type: "string_list", default: string[] }
);
type ScalarField<T> = T & {
/** 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,
};
export type Feature = {
name: string,
title: string,
description: string,
fields: FeatureField[],
};
function bool(name: string, title: string, description: string, value: boolean): FeatureField {
return { name, title, description, type: "boolean", default: value };
}
function num(name: string, title: string, description: string, value: number, min?: number, unit?: string): FeatureField {
return { name, title, description, type: "number", default: value, min, unit };
}
function str(name: string, title: string, description: string, value: string): FeatureField {
return { name, title, description, type: "string", default: value };
}
function list(name: string, title: string, description: string, value: string[]): FeatureField {
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: "number", default: null, optional: true, inputComponent: "role" };
}
export const features: Feature[] = [
{
name: "afk_kick",
title: "AFK Kick",
description: "Kick everyone when nobody online is active",
fields: [
bool("admin_as_active", "Admins count as active", "Admins are active regardless of how long they have been afk", true),
bool("trust_as_active", "Trusted players count as active", "Players with enough playtime are active regardless of how long they have been afk", true),
role("active_role_id", "Role counts as active", "Players with this role or higher are active regardless of how long they have been afk"),
num("afk_minutes", "AFK after", "Time without input before a player counts as afk", 10, 1, "minutes"),
num("kick_minutes", "Kick after", "Time without any active player before everyone is kicked", 30, 1, "minutes"),
num("trust_minutes", "Trusted after", "Playtime needed before a player counts as trusted", 600, 0, "minutes"),
],
},
{
name: "autofill",
title: "Autofill",
description: "Fill ammo and fuel into entities when they are built, configured by each player from the toolbar",
fields: [],
},
{
name: "chat_auto_reply",
title: "Chat Auto Reply",
description: "Reply to key words in chat and run chat commands",
fields: [
str("command_prefix", "Command prefix", "Prefix for chat commands, and for key words to reply to everyone", "!"),
],
},
{
name: "custom_start",
title: "Custom Start",
description: "Map settings applied when the map is created, and the items new players start with",
fields: [
bool("skip_intro", "Skip intro", "Skip the freeplay intro", true),
bool("skip_victory", "Skip victory", "Skip the victory screen when a rocket is launched", true),
bool("friendly_fire", "Friendly fire", "Players can damage each other", false),
bool("disable_crashsite", "No crash site", "Do not create the crash site", true),
bool("enemy_expansion", "Enemy expansion", "Biters expand, in case the map settings file fails to load", false),
num("chart_radius", "Chart radius", "Tiles charted around spawn when the map starts", 320, 0, "tiles"),
],
},
{
name: "deconstruction_log",
title: "Deconstruction Log",
description: "Write actions of players without the bypass permission to log/deconstruction.log",
fields: [
bool("decon_area", "Deconstructed areas", "Log when an area is deconstructed", true),
bool("built_entity", "Built entities", "Log when an entity is built", true),
bool("mined_entity", "Mined entities", "Log when an entity is mined", true),
bool("fired_rocket", "Rockets", "Log when a rocket is fired", true),
bool("fired_explosive_rocket", "Explosive rockets", "Log when an explosive rocket is fired", true),
bool("fired_nuke", "Nukes", "Log when a nuke is fired", true),
],
},
{
name: "degrading_tiles",
title: "Degrading Tiles",
description: "Tiles wear down as players walk on them and build on them",
fields: [
num("weakness_value", "Weakness", "Lower values make tiles degrade sooner", 70, 1),
list("entities", "Degrading entities", "Entities which degrade the tiles under them when built", [
"stone-furnace", "steel-furnace", "electric-furnace", "assembling-machine-1", "assembling-machine-2", "assembling-machine-3",
"beacon", "centrifuge", "chemical-plant", "oil-refinery", "storage-tank", "nuclear-reactor",
"steam-engine", "steam-turbine", "boiler", "heat-exchanger", "stone-wall", "gate",
"gun-turret", "laser-turret", "flamethrower-turret", "radar", "lab", "big-electric-pole",
"substation", "rocket-silo", "pumpjack", "electric-mining-drill", "roboport", "accumulator",
]),
],
},
{
name: "death_markers",
title: "Death Markers",
description: "What happens when players die",
fields: [
bool("collect_corpses", "Collect corpses", "Put the items of expired corpses in chests", true),
bool("show_map_markers", "Show map markers", "Add a map marker where a player died", true),
bool("clean_map_markers", "Remove map markers", "Remove the map marker once the corpse is gone", false),
bool("include_time_of_death", "Include time of death", "Add the time of death to the map marker", true),
bool("show_light_at_corpse", "Light at corpse", "Draw a light in the player's colour at their corpse", true),
bool("show_line_to_corpse", "Line to corpse", "Draw a line from a respawned player to their corpse", true),
],
},
{
name: "discord_alerts",
title: "Discord Alerts",
description: "Write alerts for the discord bot when moderation events happen",
fields: [
bool("show_playtime", "Show playtime", "Add the playtime of players to alerts", true),
bool("entity_protection", "Entity protection", "Alert when a player repeatedly removes protected entities", true),
bool("player_bans", "Bans", "Alert when a player is banned or unbanned", true),
bool("player_mutes", "Mutes", "Alert when a player is muted or unmuted", true),
bool("player_kicks", "Kicks", "Alert when a player is kicked", true),
bool("player_promotes", "Promotions", "Alert when a player is promoted or demoted", false),
bool("player_jail", "Jail", "Alert when a player is jailed or unjailed", true),
list("logged_commands", "Logged commands", "Alert when a player uses one of these commands", [
"config", "purge", "c", "command", "silent-command", "measured-command", "banlist", "permissions", "editor", "cheat",
]),
],
},
{
name: "extra_logging",
title: "Extra Logging",
description: "Write rockets, deaths, research, joins, and leaves to a log file",
fields: [
str("file_name", "File name", "Path of the log file within script-output", "log/logging.log"),
num("rocket_launch_display_rate", "Rocket log interval", "After the first few launches, log every this many rockets", 500, 1, "rockets"),
],
},
{
name: "help_bubbles",
title: "Help Bubbles",
description: "A compilatron at spawn which cycles through tips and the community links",
fields: [],
},
{
name: "inventory_clear",
title: "Inventory Clear",
description: "Move the items of banned and kicked players to a chest at spawn",
fields: [],
},
{
name: "lawnmower",
title: "Lawnmower",
description: "The /lawnmower command which clears corpses and nuclear ground",
fields: [
bool("destroy_decoratives", "Clear decoratives when building", "Remove decoratives under entities when they are built", false),
],
},
{
name: "mine_depletion",
title: "Mine Depletion",
description: "Deconstruct mining drills when their resources run out",
fields: [
bool("chest", "Remove output chests", "Also deconstruct the chest a drill outputs into", true),
bool("beacon", "Remove beacons", "Also deconstruct beacons which no longer affect anything", true),
bool("fluid", "Replace with pipes", "Place pipe ghosts where a drill carried fluid", true),
],
},
{
name: "module_inserter",
title: "Module Inserter",
description: "A tool to fill machines with modules by selecting an area",
fields: [
bool("copy_paste_module", "Copy modules", "Copying a machine also copies its modules", true),
bool("copy_paste_rotation", "Copy rotation", "Copying a machine also copies its rotation", false),
],
},
{
name: "nuke_protection",
title: "Nuke Protection",
description: "Remove banned items from players without the nuke protection bypass permission",
fields: [
list("banned_items", "Banned items", "Items which are moved to a chest at spawn", ["atomic-bomb"]),
],
},
{
name: "player_bonus",
title: "Player Bonus",
description: "Points players spend on personal bonuses, the bonuses live in config/player_bonus.lua",
fields: [
num("points.base", "Base points", "The points of the points role, the standard value of each bonus adds up to this", 174, 0, "points"),
num("points.increase_percentage_per_role_level", "Points per role level", "Extra points as a fraction of the base for each role above the points role", 0.03, 0),
role("points.role_id", "Points role", "The role the base points apply to, without one every player gets the base"),
],
},
{
name: "pollution_grading",
title: "Pollution Grading",
description: "Scale the pollution overlay so it is not one red mess",
fields: [
num("reference_point.x", "Reference X", "Where pollution is read from", 0),
num("reference_point.y", "Reference Y", "Where pollution is read from", 0),
num("max_scalar", "Max scale", "The scale between the true max and the shown max", 0.5, 0),
num("min_scalar", "Min scale", "The scale between the shown max and the shown min", 0.17, 0),
],
},
{
name: "popups",
title: "Popups",
description: "Flying text for chat messages and damage",
fields: [
bool("show_player_messages", "Chat messages", "Show chat messages above the player who sent them", true),
bool("show_player_mentions", "Mentions", "Show a popup above players mentioned in chat", true),
bool("show_player_damage", "Damage dealt", "Show the damage players deal to entities", true),
bool("show_player_health", "Player health", "Show the health of players when they are attacked", true),
num("damage_location_variance", "Damage spread", "How far from the centre of an entity damage popups appear, as a fraction of its size", 0.8, 0),
],
},
{
name: "protection",
title: "Entity Protection",
description: "Stop players removing protected entities placed by others, players with the bypass permission are ignored",
fields: [
num("repeat_count", "Repeat count", "Protected entities removed within the repeat time which count as a repeat violation", 5, 1),
num("repeat_minutes", "Repeat time", "How long removed protected entities are remembered", 20, 1, "minutes"),
list("always_protected_names", "Protected entity names", "Entities which are always protected", []),
list("always_protected_types", "Protected entity types", "Entity types which are always protected", [
"boiler", "generator", "offshore-pump", "reactor", "heat-exchanger", "heat-pipe", "fusion-reactor", "fusion-generator", "power-switch", "rocket-silo",
]),
list("always_trigger_repeat_names", "Repeat entity names", "Entities which count as a repeat violation straight away", []),
list("always_trigger_repeat_types", "Repeat entity types", "Entity types which count as a repeat violation straight away", [
"reactor", "fusion-reactor", "rocket-silo",
]),
],
},
{
name: "research",
title: "Research",
description: "Research effects, the per level limits live in config/research_data.lua",
fields: [
bool("pollution_ageing_by_research", "Pollution ageing", "Pollution ages faster with each level of the bonus inventory researches", false),
bool("bonus_inventory.enabled", "Bonus inventory", "Extra inventory slots for each level of the researches below", true),
list("bonus_inventory.res", "Bonus researches", "Researches which grant the extra slots", [
"mining-productivity", "mining-productivity-2", "mining-productivity-3", "mining-productivity-4",
]),
num("bonus_inventory.rate", "Slots per level", "Extra slots for each research level", 5, 0, "slots"),
num("bonus_inventory.limit", "Slot limit", "The most extra slots a force can have", 20, 0, "slots"),
],
},
{
name: "research_milestones",
title: "Research Milestones",
description: "A GUI tracking research against target times, the targets live in config/research_data.lua",
fields: [
str("log_file", "Log file", "Where the milestone times are written within script-output", "log/research.log"),
],
},
{
name: "rocket_info",
title: "Rocket Info",
description: "A GUI with rocket launch statistics, milestones, and build progress",
fields: [
bool("show_stats", "Statistics section", "Show the statistics section", true),
bool("show_first_rocket", "First rocket", "Show when the first rocket was launched", true),
bool("show_last_rocket", "Last rocket", "Show when the last rocket was launched", true),
bool("show_fastest_rocket", "Fastest rocket", "Show the time taken for the fastest rocket", true),
bool("show_total_rockets", "Total rockets", "Show the total number of rockets launched", true),
bool("show_game_avg", "Game average", "Show the average across the entire map time", true),
bool("show_milestones", "Milestones section", "Show when each milestone rocket was launched", true),
bool("show_progress", "Progress section", "Show the build progress of each silo", true),
bool("allow_zoom_to_map", "Zoom to map", "Clicking a silo zooms the map to it", true),
],
},
{
name: "science_production",
title: "Science Production",
description: "A GUI with the production of each science pack",
fields: [
bool("show_eta", "Show ETA", "Show the estimated time until the current research completes", true),
num("color_flux", "Colour threshold", "How much production may fluctuate before the icon changes colour", 0.1, 0),
],
},
{
name: "repair",
title: "Repair",
description: "The /repair command which revives ghosts and heals entities in an area",
fields: [
bool("allow_ghost_revive", "Revive ghosts", "Build ghosts in the area instantly", true),
bool("allow_blueprint_repair", "Revive blueprints", "Also revive ghosts which were never built, not only those left by destroyed entities", false),
bool("allow_heal_entities", "Heal entities", "Heal entities in the area to full health", true),
],
},
{
name: "spawn_area",
title: "Spawn Area",
description: "Build the spawn area when the first player joins, only the turret and resource refills apply to an existing map",
fields: [
num("spawn_area.deconstruction_radius", "Clear radius", "Entities within this radius are removed", 20, 0, "tiles"),
num("spawn_area.tile_radius", "Tile radius", "Tiles within this radius are replaced", 20, 0, "tiles"),
str("spawn_area.deconstruction_tile", "Tile", "Tile placed within the tile radius", "concrete"),
num("spawn_area.landfill_radius", "Landfill radius", "Water within this radius is filled", 50, 0, "tiles"),
bool("turrets.enabled", "Turrets", "Place turrets at spawn and keep them refilled", true),
str("turrets.ammo_type", "Turret ammo", "Ammo used to refill the turrets", "uranium-rounds-magazine"),
bool("afk_belts.enabled", "AFK belts", "Place belt loops at spawn", true),
str("afk_belts.belt_type", "Belt type", "Belt used for the loops", "transport-belt"),
bool("afk_belts.protected", "Protect belts", "Players can not remove the belts", true),
bool("water.enabled", "Water", "Place water tiles at spawn", true),
str("water.water_tile", "Water tile", "Tile used for the water", "water-mud"),
bool("entities.enabled", "Entities", "Place walls, chests, lamps, and poles at spawn", true),
bool("entities.protected", "Protect entities", "Players can not remove the entities", true),
bool("entities.operable", "Operable entities", "Players can open the entities, needed for the chests", true),
bool("pattern.enabled", "Pattern", "Place a tile pattern at spawn", true),
str("pattern.pattern_tile", "Pattern tile", "Tile used for the pattern", "stone-path"),
bool("resource_refill_nearby.enabled", "Refill resources", "Add to resources near spawn every 10 minutes", false),
num("resource_refill_nearby.range", "Refill range", "Distance from spawn which resources are refilled", 128, 0, "tiles"),
list("resource_refill_nearby.resources_name", "Refilled resources", "Resources which are refilled", [
"iron-ore", "copper-ore", "stone", "coal", "uranium-ore",
]),
],
},
{
name: "station_auto_name",
title: "Station Auto Name",
description: "Name train stops when they are built",
fields: [
str("station_name", "Station name", "Placeholders: __icon__ __item_name__ __backer_name__ __direction__ __x__ __y__", "[L] __icon__"),
],
},
{
name: "task_list",
title: "Task List",
description: "A shared list of tasks, adding and editing need the task list permissions",
fields: [
bool("user_can_edit_own_tasks", "Own tasks editable", "The player who made a task can edit it without the edit permission", true),
],
},
{
name: "warp_list",
title: "Warp List",
description: "Warp points players can travel between, adding, editing, and bypassing limits need the warp list permissions",
fields: [
num("minimum_distance", "Minimum distance", "Warps of a force can not be closer together than this", 100, 1, "tiles"),
num("cooldown_duration", "Cooldown", "Wait between warps", 60, 0, "seconds"),
num("standard_proximity_radius", "Warp radius", "How close a player must stand to a warp to use it", 4, 1, "tiles"),
num("spawn_proximity_radius", "Spawn radius", "How close a player must stand to spawn to use it as a warp", 20, 1, "tiles"),
bool("user_can_edit_own_warps", "Own warps editable", "The player who made a warp can edit it without the edit permission", false),
],
},
{
name: "vlayer",
title: "Virtual Layer",
description: "A shared virtual surface for solar panels, accumulators, and storage, the items live in config/vlayer_items.lua",
fields: [
bool("unlimited_capacity", "Unlimited capacity", "Energy storage without accumulators", false),
bool("unlimited_surface_area", "Unlimited area", "Place items without landfill", false),
bool("modded_auto_downgrade", "Downgrade modded items", "Convert modded items to their base game equivalent, the originals can not be recovered", false),
bool("power_on_space", "Power on platforms", "Energy interfaces can be built on space platforms", false),
str("power_on_space_research.name", "Platform research", "The research needed to build energy interfaces on platforms", "research-productivity"),
num("power_on_space_research.level", "Platform research level", "The level of that research", 10, 0),
str("mimic_surface", "Mimic surface", "The surface the day cycle is copied from, empty for a fixed cycle", "nauvis"),
num("interface_limit.energy", "Energy interfaces", "More than one lets disconnected power networks receive power", 1, 0),
num("interface_limit.circuit", "Circuit interfaces", "How many circuit interfaces can exist", 20, 0),
num("interface_limit.storage_input", "Input interfaces", "How many storage input interfaces can exist", 20, 0),
num("interface_limit.storage_output", "Output interfaces", "More than zero allows item teleportation of the allowed items", 1, 0),
],
},
];
/** Check values against the fields of a feature, returning those which differ from their default */
export function validateFeatureValues(name: string, values: Record<string, FeatureValue>) {
const feature = features.find(f => f.name === name);
if (!feature) {
throw new Error(`Unknown feature ${name}`);
}
const result: Record<string, FeatureValue> = {};
for (const [key, value] of Object.entries(values)) {
const field = feature.fields.find(f => f.name === key);
if (!field) {
throw new Error(`Feature ${name} has no setting ${key}`);
}
const error = fieldValueError(field, value);
if (error) {
throw new Error(`Setting ${key} of ${name} ${error}`);
}
if (!isDefaultValue(field, value)) {
result[key] = value;
}
}
return result;
}
/** Why a value does not fit a field, undefined when it does */
function fieldValueError(field: FeatureField, value: FeatureValue) {
if (value === null) {
return field.type === "string_list" || !field.optional ? "can not be null" : undefined;
}
switch (field.type) {
case "string_list":
if (!Array.isArray(value) || value.some(item => typeof item !== "string")) {
return "must be a list of strings";
}
return undefined;
case "number":
if (typeof value !== "number" || !Number.isFinite(value)) {
return "must be a finite number";
}
if (field.min !== undefined && value < field.min) {
return `must be at least ${field.min}`;
}
return undefined;
case "string":
if (typeof value !== "string") {
return "must be a string";
}
if (field.enum && !field.enum.includes(value)) {
return `must be one of ${field.enum.join(", ")}`;
}
return undefined;
default:
return typeof value === field.type ? undefined : `must be a ${field.type}`;
}
}
/** Check if two values are equal, lists are compared by content. */
export function sameValue(a: FeatureValue | undefined, b: FeatureValue | undefined) {
if (Array.isArray(a) && Array.isArray(b)) {
return a.length === b.length && a.every((item, index) => item === b[index]);
}
return a === b;
}
export function isDefaultValue(field: FeatureField, value: FeatureValue) {
return sameValue(value, field.default);
}
/** Split stored values into those which still fit a field and the names of the rest */
export function pruneFeatureValues(feature: Feature, values: Record<string, FeatureValue>) {
const kept: Record<string, FeatureValue> = {};
const dropped: string[] = [];
for (const [key, value] of Object.entries(values)) {
const field = feature.fields.find(f => f.name === key);
if (field && !fieldValueError(field, value)) {
kept[key] = value;
} else {
dropped.push(key);
}
}
return { kept, dropped };
}
+6
View File
@@ -20,6 +20,9 @@ export const plugin: lib.PluginDeclaration = {
messages: [
messages.SeedRequest,
messages.FeatureUpdatedEvent,
messages.FeatureListRequest,
messages.FeatureUpdateRequest,
],
permissions: [
@@ -42,4 +45,7 @@ export const plugin: lib.PluginDeclaration = {
],
webEntrypoint: "./web",
routes: [
"/scenario_features",
],
};
+86 -3
View File
@@ -1,3 +1,86 @@
// Empty because an instance entrypoint must be defined for a module to be injected
// This requirement may change in the future to allow for standalone modules
export default async function () {}
import type { Instance, InstancePluginContext } from "@clusterio/host";
import * as lib from "@clusterio/lib";
import * as messages from "./messages.js";
import { features, FeatureValue } from "./features.js";
/** Sends feature config to the lua side, see module/features.lua. */
export class InstancePlugin {
/** Rcon is up and the lua side can take updates */
started = false;
private constructor(
public instance: Instance,
public logger: lib.Logger,
public name: string,
) {}
static async fromContext(context: InstancePluginContext) {
const instance = context.instance;
const plugin = new InstancePlugin(instance, context.logger, context.plugin.name);
instance.handle(messages.FeatureUpdatedEvent, plugin.handleFeatureUpdatedEvent.bind(plugin));
instance.hooks.start.attach(plugin.name, plugin.onStart.bind(plugin));
instance.hooks.exit.attach(plugin.name, plugin.onExit.bind(plugin));
instance.hooks.controllerConnectionEvent.attach(plugin.name, plugin.onControllerConnectionEvent.bind(plugin));
return plugin;
}
async onStart() {
this.started = true;
await this.syncFeatures();
}
async onExit() {
this.started = false;
}
/** A new session means the controller restarted and forgot the subscription. */
async onControllerConnectionEvent(event: "connect" | "drop" | "resume" | "close") {
if (event === "connect" && this.started) {
await this.syncFeatures();
}
}
/** Subscribe, then send every feature */
async syncFeatures() {
await this.instance.sendTo("controller", new lib.SubscriptionRequest(
`exp_scenario:${messages.FeatureUpdatedEvent.name}`, "subscribe", Date.now(),
));
const features = await this.instance.sendTo("controller", new messages.FeatureListRequest());
await this.luaSendFeatures(features);
}
async handleFeatureUpdatedEvent(event: messages.FeatureUpdatedEvent) {
if (this.started) {
await this.luaSendFeatures(event.updates.filter(feature => !feature.isDeleted));
}
}
/** Every value of each feature is sent, so the lua side sets them all */
async luaSendFeatures(records: messages.FeatureRecord[]) {
if (!records.length) {
return;
}
const updates = records.map(record => {
const values: Record<string, FeatureValue> = { enabled: record.enabled };
for (const field of features.find(feature => feature.name === record.id)?.fields ?? []) {
values[field.name] = field.name in record.values ? record.values[field.name] : field.default;
}
return { name: record.id, values };
});
const json = JSON.stringify(updates);
// Pick a long bracket the json does not contain
let level = "=";
while (json.includes(`]${level}]`)) {
level += "=";
}
await this.instance.sendRcon(
`/sc exp_scenario.features.receive_update(helpers.json_to_table[${level}[${json}]${level}])`, true,
);
}
}
export default async function (context: InstancePluginContext) {
await InstancePlugin.fromContext(context);
}
+117
View File
@@ -1,3 +1,7 @@
import * as lib from "@clusterio/lib";
import { Type, Static } from "@sinclair/typebox";
import type { FeatureValue } from "./features.js";
/** Create the roles and permission groups the scenario shipped with, see seed.ts. */
export class SeedRequest {
declare ["constructor"]: typeof SeedRequest;
@@ -9,3 +13,116 @@ export class SeedRequest {
constructor() {}
}
const FeatureValueSchema = Type.Union([Type.Boolean(), Type.Number(), Type.String(), Type.Array(Type.String()), Type.Null()]);
/** The config of one feature, see features.ts. Only values which differ from the default are stored. */
export class FeatureRecord {
constructor(
/** Name of the feature in features.ts */
public id: string,
public enabled: boolean,
public values: Record<string, FeatureValue> = {},
public updatedAtMs: number = 0,
public isDeleted: boolean = false,
) {}
static jsonSchema = Type.Object({
id: Type.String(),
enabled: Type.Boolean(),
values: Type.Record(Type.String(), FeatureValueSchema),
updated_at_ms: Type.Optional(Type.Number()),
is_deleted: Type.Optional(Type.Boolean()),
});
toJSON() {
const json: Static<typeof FeatureRecord.jsonSchema> = {
id: this.id,
enabled: this.enabled,
values: this.values,
};
if (this.updatedAtMs) {
json.updated_at_ms = this.updatedAtMs;
}
if (this.isDeleted) {
json.is_deleted = true;
}
return json;
}
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(
json.id,
json.enabled,
json.values,
json.updated_at_ms ?? 0,
json.is_deleted ?? false,
);
}
}
export class FeatureUpdatedEvent {
declare ["constructor"]: typeof FeatureUpdatedEvent;
static plugin = "exp_scenario" as const;
static type = "event" as const;
static src = "controller" as const;
static dst = ["control", "instance"] as const;
static permission = "exp_scenario.config.view" as const;
constructor(
public updates: FeatureRecord[],
) {}
static jsonSchema = Type.Object({
updates: Type.Array(FeatureRecord.jsonSchema),
});
toJSON() {
return { updates: this.updates.map(feature => feature.toJSON()) };
}
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.updates.map(feature => FeatureRecord.fromJSON(feature)));
}
}
/** List the config of every feature. */
export class FeatureListRequest {
declare ["constructor"]: typeof FeatureListRequest;
static plugin = "exp_scenario" as const;
static type = "request" as const;
static src = ["control", "instance"] as const;
static dst = "controller" as const;
static permission = "exp_scenario.config.view" as const;
static Response = lib.jsonArray(FeatureRecord);
}
/** Replace the config of a feature, values left out go back to their default. */
export class FeatureUpdateRequest {
declare ["constructor"]: typeof FeatureUpdateRequest;
static plugin = "exp_scenario" as const;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "exp_scenario.config.edit" as const;
static Response = FeatureRecord;
constructor(
public id: string,
public enabled: boolean,
public values: Record<string, FeatureValue>,
) {}
static jsonSchema = Type.Object({
id: Type.String(),
enabled: Type.Boolean(),
values: Type.Record(Type.String(), FeatureValueSchema),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.id, json.enabled, json.values);
}
}
@@ -5,6 +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 Feature = require("modules/exp_scenario/features")
local Roles = require("modules/exp_roles")
local player_has_permission = Roles.player_has_permission
@@ -22,6 +23,17 @@ authorities.exp_permission =
end
end)
--- Commands flagged with a feature are denied while it is disabled, see features.lua
authorities.feature_enabled =
add(function(player, command)
local feature = command.flags.feature
if feature and not feature:is_enabled() then
return deny{ "exp-commands-authorities_feature.deny" }
else
return allow()
end
end)
Roles.define_permission_trigger("core.admin", function(player, state)
if state then
Commands.unlock_system_commands(player.name)
+14 -11
View File
@@ -4,9 +4,13 @@ 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 Feature = require("modules/exp_scenario/features")
local Selection = require("modules/exp_util/selection")
local SelectArea = Selection.connect("ExpCommand_Lawnmower")
local config = require("modules.exp_legacy.config.lawnmower")
local feature, config = Feature.register("lawnmower", {
destroy_decoratives = false, -- remove decoratives under entities when they are built
})
--- @class ExpCommand_Lawnmower.commands
local commands = {}
@@ -15,6 +19,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 = feature }
:register(function(player)
if SelectArea:stop(player) then
return Commands.status.success{ "exp_util.selection_exit", { "exp-commands_lawnmower.selection-name" } }
@@ -51,6 +56,7 @@ end)
--- @param event EventData.on_built_entity | EventData.on_robot_built_entity | EventData.script_raised_built | EventData.script_raised_revive
local function destroy_decoratives(event)
if not config.destroy_decoratives then return end
local entity = event.entity
if entity.type ~= "entity-ghost" and entity.type ~= "tile-ghost" and entity.prototype.selectable_in_game then
entity.surface.destroy_decoratives{ area = entity.selection_box }
@@ -58,16 +64,13 @@ local function destroy_decoratives(event)
end
local e = defines.events
local events = {}
if config.destroy_decoratives then
events[e.on_built_entity] = destroy_decoratives
events[e.on_robot_built_entity] = destroy_decoratives
events[e.script_raised_built] = destroy_decoratives
events[e.script_raised_revive] = destroy_decoratives
end
return {
events = events,
return feature:guard{
events = {
[e.on_built_entity] = destroy_decoratives,
[e.on_robot_built_entity] = destroy_decoratives,
[e.script_raised_built] = destroy_decoratives,
[e.script_raised_revive] = destroy_decoratives,
},
commands = commands,
}
@@ -153,6 +153,17 @@ SelectArea:on_alt_selection(function(event)
end
end)
--- The keys of a set as a list, find_entities_filtered takes a list of names
--- @param set table<string, true>
--- @return string[]
local function set_keys(set)
local keys = {}
for key in pairs(set) do
keys[#keys + 1] = key
end
return keys
end
--- When selection starts show all protected entities and protected areas
local function on_player_selection_start(event)
local player = game.players[event.player_index]
@@ -166,15 +177,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 = set_keys(EntityProtection.config.always_protected_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 = set_keys(EntityProtection.config.always_protected_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
+10 -3
View File
@@ -4,9 +4,15 @@ Adds a command that allows an admin to repair and revive a large area
local AABB = require("modules/exp_util/aabb")
local Commands = require("modules/exp_commands")
local config = require("modules.exp_legacy.config.repair") --- @dep config.repair
local Feature = require("modules/exp_scenario/features")
local Selection = require("modules/exp_util/selection")
local SelectArea = Selection.connect("ExpCommand_Waterfill")
local SelectArea = Selection.connect("ExpCommand_Repair")
local feature, config = Feature.register("repair", {
allow_blueprint_repair = false, -- revive ghosts which were never built, not only those left by destroyed entities
allow_ghost_revive = true, -- revive ghosts in the area
allow_heal_entities = true, -- heal entities in the area to full health
})
--- @class ExpCommands_Repair.commands
local commands = {}
@@ -14,6 +20,7 @@ local commands = {}
--- Toggle player selection mode
--- @class ExpCommands_Repair.commands.repair: ExpCommand
commands.repair = Commands.new("repair", { "exp-commands_repair.description" })
:add_flags{ feature = feature }
:register(function(player)
if SelectArea:stop(player) then
return Commands.status.success{ "exp_util.selection_exit", { "exp-commands_repair.selection-name" } }
@@ -22,7 +29,7 @@ commands.repair = Commands.new("repair", { "exp-commands_repair.description" })
return Commands.status.success{ "exp_util.selection_enter", { "exp-commands_repair.selection-name" } }
end)
--- When an area is selected to be converted to water
--- When an area is selected to be repaired
SelectArea:on_selection(function(event)
local player = assert(game.get_player(event.player_index))
local area = AABB.expand(event.area)
@@ -3,7 +3,7 @@
local config = {
-- General config
icon = "item/piercing-rounds-magazine", -- @setting icon that will be used for the toolbar
icon = "item/piercing-rounds-magazine", -- that will be used for the toolbar
categories = {
ammo = "ammo",
fuel = "fuel",
@@ -1,7 +1,8 @@
--- This file defines the different triggers for the chat bot
-- @config Chat-Reply
-- @config Chat-Auto-Reply
local ExpUtil = require("modules/exp_util")
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
@@ -24,9 +25,9 @@ local afk_time_units = {
seconds = true,
}
return {
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
messages = { -- will trigger when ever the word is said
["discord"] = { "info.discord", config_server_detail["discord"] },
["expgaming"] = { "info.website", config_server_detail["website"] },
["website"] = { "info.website", config_server_detail["website"] },
@@ -64,12 +65,10 @@ return {
return { "exp_chat-auto-reply.reply-afk", max.name, ExpUtil.format_time_locale(max.afk_time, "long", afk_time_units) }
end,
},
allow_command_prefix_for_messages = true, --- @setting allow_command_prefix_for_messages when true any message trigger will print to all player when prefixed
command_admin_only = false, --- @setting command_admin_only when true will only allow chat commands for admins
command_permission = "exp_scenario.chat.commands", --- @setting command_permission the permission used to allow command prefixes
command_prefix = "!", --- @setting command_prefix prefix used for commands below and to print to all players (if enabled above)
allow_command_prefix_for_messages = true, -- when true any message trigger will print to all player when prefixed
command_prefix = "!", -- prefix used for commands below and to print to all players (if enabled above)
--- @type table<string, LocalisedString | fun(player: LuaPlayer, is_command: boolean): LocalisedString>
commands = { --- @setting commands will trigger only when command prefix is given
commands = { -- will trigger only when command prefix is given
["dev"] = { "exp_chat-auto-reply.reply-dev" },
["magic"] = { "exp_chat-auto-reply.reply-magic" },
["aids"] = { "exp_chat-auto-reply.reply-aids" },
@@ -134,4 +133,4 @@ return {
return { locale_reply, { "exp_chat-auto-reply.reply-beer-1" } }
end,
},
}
})
@@ -0,0 +1,82 @@
--- The machines the module inserter can fill, and the modules it puts in them, by mod set
-- @config Module-Inserter-Machines
return {
["base"] = {
["electric-mining-drill"] = {
["module"] = "efficiency-module",
["prod"] = true,
},
["pumpjack"] = {
["module"] = "efficiency-module",
["prod"] = true,
},
["assembling-machine-2"] = {
["module"] = "productivity-module",
["prod"] = true,
},
["assembling-machine-3"] = {
["module"] = "productivity-module-3",
["prod"] = true,
},
["electric-furnace"] = {
["module"] = "productivity-module-3",
["prod"] = true,
},
["beacon"] = {
["module"] = "speed-module-3",
["prod"] = false,
},
["oil-refinery"] = {
["module"] = "productivity-module-3",
["prod"] = true,
},
["chemical-plant"] = {
["module"] = "productivity-module-3",
["prod"] = true,
},
["centrifuge"] = {
["module"] = "productivity-module-3",
["prod"] = true,
},
["lab"] = {
["module"] = "productivity-module-3",
["prod"] = true,
},
["rocket-silo"] = {
["module"] = "productivity-module-3",
["prod"] = true,
}
},
["space-age"] = {
["big-mining-drill"] = {
["module"] = "efficiency-module",
["prod"] = true,
},
["biochamber"] = {
["module"] = "efficiency-module",
["prod"] = true,
},
["electromagnetic-plant"] = {
["module"] = "productivity-module-3",
["prod"] = true,
},
["cryogenic-plant"] = {
["module"] = "productivity-module-3",
["prod"] = true,
},
["biolab"] = {
["module"] = "productivity-module-3",
["prod"] = true,
},
["foundry"] = {
["module"] = "productivity-module-3",
["prod"] = true,
},
["recycler"] = {
["module"] = "quality-module-3",
["prod"] = true,
}
}
}
@@ -0,0 +1,63 @@
--- The bonuses players can spend their points on, see gui/player_bonus.lua
-- @config Player-Bonus
return {
{
name = "character_mining_speed_modifier",
scale = 1,
cost = 10,
max_value = 4,
value_step = 0.5,
is_percentage = true,
},
{
name = "character_running_speed_modifier",
scale = 1,
cost = 60,
max_value = 2,
value_step = 0.25,
is_percentage = true,
},
{
name = "character_crafting_speed_modifier",
scale = 1,
cost = 4,
max_value = 12,
value_step = 1,
is_percentage = true,
},
{
name = "character_inventory_slots_bonus",
scale = 10,
cost = 2,
max_value = 100,
value_step = 10,
},
{
name = "character_health_bonus",
scale = 50,
cost = 4,
max_value = 300,
value_step = 50,
},
{
name = "character_reach_distance_bonus",
scale = 1,
cost = 1,
max_value = 16,
value_step = 2,
combined_bonus = {
"character_resource_reach_distance_bonus",
"character_build_distance_bonus",
},
},
{
name = "personal_battery_recharge",
scale = 4,
cost = 40,
max_value = 8,
value_step = 1,
is_special = true,
},
}
@@ -1,40 +1,20 @@
--- Res Settings
-- @config Research
--- Research data by mod set, the first set in mod_set_lookup whose mod is active is used
-- @config Research-Data
return {
enabled = true,
pollution_ageing_by_research = false,
queue_amount = 3,
mod_set = "base",
local data = {
mod_set_lookup = {
"space-age",
},
-- this enable 20 more inventory for each mining productivity level up to 4
bonus_inventory = {
enabled = true,
res = {
-- Mining Productivity
["mining-productivity"] = true,
["mining-productivity-2"] = true,
["mining-productivity-3"] = true,
["mining-productivity-4"] = true,
final_res = {
["base"] = {
["name"] = "mining-productivity-4",
["level"] = 4
},
name = "character_inventory_slots_bonus",
rate = 5,
limit = 20,
},
file = {
name = "log/research.log",
final_res = {
["base"] = {
["name"] = "mining-productivity-4",
["level"] = 4
},
["space-age"] = {
["name"] = "mining-productivity-3",
["level"] = 3
}
["space-age"] = {
["name"] = "mining-productivity-3",
["level"] = 3
}
},
milestone = {
["base"] = {
@@ -177,3 +157,13 @@ return {
-- ["atomic-bomb"] = 1
},
}
data.mod_set = "base"
for _, mod_name in ipairs(data.mod_set_lookup) do
if script.active_mods[mod_name] then
data.mod_set = mod_name
break
end
end
return data
@@ -1,41 +1,42 @@
--- 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
return {
spawn_area = { --- @setting spawn_area Settings relating to the whole spawn area
local Feature = require("modules/exp_scenario/features")
return Feature.register("spawn_area", {
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
deconstruction_radius = 20, -- All entities within this radius will be removed
tile_radius = 20,
deconstruction_tile = "concrete", --- @setting deconstruction_tile Tile to be placed in the deconstruction radius, use nil for map gen
landfill_radius = 50, --- @setting pattern_radius All water within this radius will be land filled
deconstruction_tile = "concrete", -- Tile to be placed in the deconstruction radius, use nil for map gen
landfill_radius = 50, -- All water within this radius will be land filled
},
turrets = { --- @setting turrets Settings relating to adding turrets to spawn
enabled = true, --- @setting enabled Whether turrets will be added to spawn
ammo_type = "uranium-rounds-magazine", --- @setting ammo_type The ammo type that will be used during refills
refill_time = 60 * 60 * 5, --- @setting refill_time The time in ticks between each refill of the turrets, only change if having lag issues
offset = { x = 0, y = 0 }, --- @setting offset The position offset to apply to turrets
locations = { --- @setting locations The locations of all turrets, this list can change during runtime
turrets = { -- Settings relating to adding turrets to spawn
enabled = true, -- Whether turrets will be added to spawn
ammo_type = "uranium-rounds-magazine", -- The ammo type that will be used during refills
offset = { x = 0, y = 0 }, -- The position offset to apply to turrets
locations = { -- The locations of all turrets, this list can change during runtime
{ -3, -3 },
{ 3, -3 },
{ -3, 3 },
{ 3, 3 },
},
},
afk_belts = { --- @setting afk_belts Settings relating to adding afk belts to spawn
enabled = true, --- @setting enabled Whether afk belts will be added to spawn
belt_type = "transport-belt", --- @setting belt_type The belt to be used as afk belts
protected = true, --- @setting protected Whether belts will be protected from player interaction
offset = { x = 0, y = 0 }, --- @setting offset The position offset to apply to afk belts
locations = { --- @setting locations The locations to spawn afk belts at, given as the top left position
afk_belts = { -- Settings relating to adding afk belts to spawn
enabled = true, -- Whether afk belts will be added to spawn
belt_type = "transport-belt", -- The belt to be used as afk belts
protected = true, -- Whether belts will be protected from player interaction
offset = { x = 0, y = 0 }, -- The position offset to apply to afk belts
locations = { -- The locations to spawn afk belts at, given as the top left position
{ -5, -5 }, { 5, -5 },
{ -5, 5 }, { 5, 5 },
},
},
water = { --- @setting water Settings relating to adding water to spawn
enabled = true, --- @setting enabled Whether water tiles will be added to spawn
water_tile = "water-mud", --- @setting water_tile The tile to be used as the water tile
offset = { x = 0, y = 0 }, --- @setting offset The position offset to apply to water tiles
locations = { --- @setting locations The location of the water tiles {x,y}
water = { -- Settings relating to adding water to spawn
enabled = true, -- Whether water tiles will be added to spawn
water_tile = "water-mud", -- The tile to be used as the water tile
offset = { x = 0, y = 0 }, -- The position offset to apply to water tiles
locations = { -- The location of the water tiles {x,y}
-- Each is a 3x3 with the closest tile to 0,0 removed
{ 7, 8 }, { 7, 9 }, { 8, 7 }, { 8, 8 }, { 8, 9 }, { 9, 7 }, { 9, 8 }, { 9, 9 }, -- Bottom Right
{ 7, -9 }, { 7, -10 }, { 8, -8 }, { 8, -9 }, { 8, -10 }, { 9, -8 }, { 9, -9 }, { 9, -10 }, -- Top Right
@@ -43,12 +44,12 @@ return {
{ -8, 8 }, { -8, 9 }, { -9, 7 }, { -9, 8 }, { -9, 9 }, { -10, 7 }, { -10, 8 }, { -10, 9 }, -- Bottom Left
},
},
entities = { --- @setting entities Settings relating to adding entities to spawn
enabled = true, --- @setting enabled Whether entities will be added to spawn
protected = true, --- @setting protected Whether entities will be protected from player interaction
operable = true, --- @setting operable Whether entities can be opened by players, must be true if chests are used
offset = { x = 0, y = -2 }, --- @setting offset The position offset to apply to entities
locations = { --- @setting locations The location and names of entities {name,x,y}
entities = { -- Settings relating to adding entities to spawn
enabled = true, -- Whether entities will be added to spawn
protected = true, -- Whether entities will be protected from player interaction
operable = true, -- Whether entities can be opened by players, must be true if chests are used
offset = { x = 0, y = -2 }, -- The position offset to apply to entities
locations = { -- The location and names of entities {name,x,y}
{ "stone-wall", -10, -5 }, { "stone-wall", -10, -4 }, { "stone-wall", -10, -3 }, { "stone-wall", -10, -2 }, { "stone-wall", -10, -1 }, { "stone-wall", -10, 0 }, { "stone-wall", -10, 3 }, { "stone-wall", -10, 4 }, { "stone-wall", -10, 5 },
{ "stone-wall", -10, 6 }, { "stone-wall", -10, 7 }, { "stone-wall", -10, 8 }, { "small-lamp", -8, -4 }, { "small-lamp", -8, -1 }, { "iron-chest", -8, 0 }, { "iron-chest", -8, 3 }, { "small-lamp", -8, 4 },
{ "small-lamp", -8, 7 }, { "stone-wall", -7, -8 }, { "small-electric-pole", -7, -2 }, { "iron-chest", -7, 0 }, { "iron-chest", -7, 3 }, { "small-electric-pole", -7, 5 }, { "stone-wall", -7, 11 }, { "stone-wall", -6, -8 }, { "small-lamp", -6, -6 },
@@ -66,10 +67,10 @@ return {
},
},
pattern = {
enabled = true, --- @setting enabled Whether pattern tiles will be added to spawn
pattern_tile = "stone-path", --- @setting pattern_tile The tile to be used for the pattern
offset = { x = 0, y = -2 }, --- @setting offset The position offset to apply to pattern tiles
locations = { --- @setting locations The location of the pattern tiles {x,y}
enabled = true, -- Whether pattern tiles will be added to spawn
pattern_tile = "stone-path", -- The tile to be used for the pattern
offset = { x = 0, y = -2 }, -- The position offset to apply to pattern tiles
locations = { -- The location of the pattern tiles {x,y}
{ -49, -3 }, { -49, -2 }, { -49, 1 }, { -49, 2 }, { -49, 5 }, { -49, 6 }, { -48, -4 }, { -48, -3 }, { -48, -2 }, { -48, 1 }, { -48, 2 }, { -48, 5 }, { -48, 6 }, { -48, 7 }, { -47, -7 }, { -47, -6 }, { -47, -5 }, { -47, -4 }, { -47, -3 }, { -47, -2 }, { -47, 5 }, { -47, 6 }, { -47, 7 }, { -47, 8 }, { -47, 9 }, { -47, 10 }, { -46, -8 }, { -46, -7 }, { -46, -6 }, { -46, -5 },
{ -46, -4 }, { -46, -3 }, { -46, -2 }, { -46, -1 }, { -46, 4 }, { -46, 5 }, { -46, 6 }, { -46, 7 }, { -46, 8 }, { -46, 9 }, { -46, 10 }, { -46, 11 }, { -45, -17 }, { -45, -16 }, { -45, -15 }, { -45, -14 }, { -45, -13 }, { -45, -12 }, { -45, -9 }, { -45, -8 }, { -45, -7 }, { -45, -2 }, { -45, -1 }, { -45, 0 }, { -45, 1 }, { -45, 2 }, { -45, 3 }, { -45, 4 }, { -45, 5 }, { -45, 10 },
{ -45, 11 }, { -45, 12 }, { -45, 15 }, { -45, 16 }, { -45, 17 }, { -45, 18 }, { -45, 19 }, { -45, 20 }, { -44, -18 }, { -44, -17 }, { -44, -16 }, { -44, -15 }, { -44, -14 }, { -44, -13 }, { -44, -12 }, { -44, -9 }, { -44, -8 }, { -44, -1 }, { -44, 0 }, { -44, 1 }, { -44, 2 }, { -44, 3 }, { -44, 4 }, { -44, 11 }, { -44, 12 }, { -44, 15 }, { -44, 16 }, { -44, 17 }, { -44, 18 }, { -44, 19 },
@@ -242,8 +243,7 @@ return {
resource_refill_nearby = {
enabled = false,
range = 128,
refill_time = 36000,
resources_name = {
resources_name = Feature.list{
"iron-ore",
"copper-ore",
"stone",
@@ -252,4 +252,4 @@ return {
},
amount = { 2500, 4000 },
},
}
})
@@ -1,14 +1,12 @@
--- This file is used to setup the map starting settings and the items players will start with
-- @config Advanced-Start
--- The items players start with, and how many depending on what the factory has made
-- @config Starting-Items
--- These are called factories because they return another function
-- use these as a simple methods of adding new items
-- they will do most of the work for you
-- ['item-name'] = factory(params)
-- luacheck:ignore 212/amount_made 212/items_made 212/player
-- Use these to adjust for ticks ie game.tick < 5*minutes
-- luacheck:ignore 211/seconds 211/minutes 211/hours
local seconds, minutes, hours = 60, 3600, 216000
--- Use to make a split point for the number of items given based on time
@@ -72,41 +70,34 @@ end
hours, minutes, seconds are the number of ticks in each unit of time
]]
--- @type table<string, number | fun(amount_made: number, items_made: fun(name: string): number, player: LuaPlayer): number>
return {
skip_intro = true, --- @setting skip_intro skips the intro given in the default factorio free play scenario
skip_victory = true, --- @setting skip_victory will skip the victory screen when a rocket is launched
friendly_fire = false, --- @setting friendly_fire weather players will be able to attack each other on the same force
disable_crashsite = true, --- @setting disable_crashsite weather to disable creation of the crashsite
enemy_expansion = false, --- @setting enemy_expansion a catch all for in case the map settings file fails to load
chart_radius = 10 * 32, --- @setting chart_radius the number of tiles that will be charted when the map starts
items = { --- @setting items items and there condition for being given
-- ['item-name'] = function(amount_made, production_stats, player) return <Number> end -- 0 means no items given
-- Plates
["iron-plate"] = scale_amount_made(100, 10, 10),
["copper-plate"] = scale_amount_made(100, 0, 8),
["steel-plate"] = scale_amount_made(100, 0, 4),
-- Secondary Items
["electronic-circuit"] = scale_amount_made(1000, 0, 6),
["iron-gear-wheel"] = scale_amount_made(1000, 0, 6),
-- Starting Items
["burner-mining-drill"] = cutoff_time(10 * minutes, 4, 0),
["stone-furnace"] = cutoff_time(10 * minutes, 4, 0),
-- Armor
["light-armor"] = cutoff_amount_made_unless(5, 0, 1, "heavy-armor", 5),
["heavy-armor"] = cutoff_amount_made(5, 0, 1),
-- Weapon
["pistol"] = cutoff_amount_made_unless(0, 1, 1, "submachine-gun", 5),
["submachine-gun"] = cutoff_amount_made(5, 0, 1),
-- Ammo
["firearm-magazine"] = cutoff_amount_made_unless(100, 10, 0, "piercing-rounds-magazine", 100),
["piercing-rounds-magazine"] = cutoff_amount_made(100, 0, 10),
--[[
['construction-robot'] = 10,
['modular-armor'] = 1,
['solar-panel-equipment'] = 16,
['belt-immunity-equipment'] = 1,
['battery-equipment'] = 2,
['personal-roboport-equipment'] = 1
]]
}
-- ['item-name'] = function(amount_made, production_stats, player) return <Number> end -- 0 means no items given
-- Plates
["iron-plate"] = scale_amount_made(100, 10, 10),
["copper-plate"] = scale_amount_made(100, 0, 8),
["steel-plate"] = scale_amount_made(100, 0, 4),
-- Secondary Items
["electronic-circuit"] = scale_amount_made(1000, 0, 6),
["iron-gear-wheel"] = scale_amount_made(1000, 0, 6),
-- Starting Items
["burner-mining-drill"] = cutoff_time(10 * minutes, 4, 0),
["stone-furnace"] = cutoff_time(10 * minutes, 4, 0),
-- Armor
["light-armor"] = cutoff_amount_made_unless(5, 0, 1, "heavy-armor", 5),
["heavy-armor"] = cutoff_amount_made(5, 0, 1),
-- Weapon
["pistol"] = cutoff_amount_made_unless(0, 1, 1, "submachine-gun", 5),
["submachine-gun"] = cutoff_amount_made(5, 0, 1),
-- Ammo
["firearm-magazine"] = cutoff_amount_made_unless(100, 10, 0, "piercing-rounds-magazine", 100),
["piercing-rounds-magazine"] = cutoff_amount_made(100, 0, 10),
--[[
['construction-robot'] = 10,
['modular-armor'] = 1,
['solar-panel-equipment'] = 16,
['belt-immunity-equipment'] = 1,
['battery-equipment'] = 2,
['personal-roboport-equipment'] = 1
]]
}
+147
View File
@@ -0,0 +1,147 @@
--- The items the vlayer can hold and what they do, see control/vlayer.lua
-- @config Vlayer-Items
return {
--- @class Vlayer.ItemProperties
--- @field starting_value number
--- @field required_area number
--- @field production number?
--- @field discharge number?
--- @field capacity number?
--- @field surface_area number?
--- @field fuel_value number?
--- @field power boolean?
--- @field modded boolean? Set when the item is registered from a modded equivalent
--- @field base_game_equivalent string?
--- @field multiplier number?
--- @type table<string, Vlayer.ItemProperties>
allowed_items = { -- List of all items allowed in vlayer storage and their properties
--[[
Allowed properties:
starting_value : The amount of the item placed into the vlayer on game start, ignores area requirements
required_area : When greater than 0 the items properties are not applied unless their is sufficient surplus surface area
production : The energy production of the item in MW, used for solar panels
discharge : The energy discharge of the item in MW, used for accumulators
capacity : The energy capacity of the item in MJ, used for accumulators
surface_area : The surface area provided by the item, used for landfill
]]
["solar-panel"] = {
starting_value = 0,
required_area = 9,
production = 0.06, -- MW
},
["accumulator"] = {
starting_value = 0,
required_area = 4,
discharge = 0.3, -- MW
capacity = 5, -- MJ
},
["landfill"] = {
starting_value = 0,
required_area = 0,
surface_area = 20, -- Tiles
},
["wood"] = {
starting_value = 0,
required_area = 0,
surface_area = 0,
fuel_value = 2, -- MJ
power = true, -- turn all wood to power to reduce trash
},
["coal"] = {
starting_value = 0,
required_area = 0,
surface_area = 0,
fuel_value = 4, -- MJ
power = false, -- turn all coal to power to reduce trash
},
["solid-fuel"] = {
starting_value = 0,
required_area = 0,
surface_area = 0,
fuel_value = 12, -- MJ
power = false, -- turn all solid fuel to power to reduce trash
},
["rocket-fuel"] = {
starting_value = 0,
required_area = 0,
surface_area = 0,
fuel_value = 100, -- MJ
power = false, -- turn all rocket fuel to power to reduce trash
}
},
modded_items = { -- List of all modded items allowed in vlayer storage and their base game equivalent
["solar-panel-2"] = {
starting_value = 0,
base_game_equivalent = "solar-panel",
multiplier = 4,
},
["solar-panel-3"] = {
starting_value = 0,
base_game_equivalent = "solar-panel",
multiplier = 16,
},
["solar-panel-4"] = {
starting_value = 0,
base_game_equivalent = "solar-panel",
multiplier = 64,
},
["solar-panel-5"] = {
starting_value = 0,
base_game_equivalent = "solar-panel",
multiplier = 256,
},
["solar-panel-6"] = {
starting_value = 0,
base_game_equivalent = "solar-panel",
multiplier = 1024,
},
["solar-panel-7"] = {
starting_value = 0,
base_game_equivalent = "solar-panel",
multiplier = 4096,
},
["solar-panel-8"] = {
starting_value = 0,
base_game_equivalent = "solar-panel",
multiplier = 16384,
},
["accumulator-2"] = {
starting_value = 0,
base_game_equivalent = "accumulator",
multiplier = 4,
},
["accumulator-3"] = {
starting_value = 0,
base_game_equivalent = "accumulator",
multiplier = 16,
},
["accumulator-4"] = {
starting_value = 0,
base_game_equivalent = "accumulator",
multiplier = 64,
},
["accumulator-5"] = {
starting_value = 0,
base_game_equivalent = "accumulator",
multiplier = 256,
},
["accumulator-6"] = {
starting_value = 0,
base_game_equivalent = "accumulator",
multiplier = 1024,
},
["accumulator-7"] = {
starting_value = 0,
base_game_equivalent = "accumulator",
multiplier = 4096,
},
["accumulator-8"] = {
starting_value = 0,
base_game_equivalent = "accumulator",
multiplier = 16384,
},
}
}
+3 -3
View File
@@ -11,6 +11,7 @@ require("modules/exp_scenario/commands/_types")
--- Commands with events
add(require("modules/exp_scenario/commands/protected_entities"))
add(require("modules/exp_scenario/commands/protected_tags"))
add(require("modules/exp_scenario/commands/lawnmower"))
--- Commands
require("modules/exp_scenario/commands/admin_chat")
@@ -24,7 +25,6 @@ require("modules/exp_scenario/commands/enemy")
require("modules/exp_scenario/commands/home")
require("modules/exp_scenario/commands/jail")
require("modules/exp_scenario/commands/kill")
require("modules/exp_scenario/commands/lawnmower")
require("modules/exp_scenario/commands/locate")
require("modules/exp_scenario/commands/me")
require("modules/exp_scenario/commands/rainbow")
@@ -43,13 +43,12 @@ require("modules/exp_scenario/commands/waterfill")
add(require("modules/exp_scenario/control/afk_kick"))
add(require("modules/exp_scenario/control/bonus"))
add(require("modules/exp_scenario/control/chat_auto_reply"))
add(require("modules/exp_scenario/control/chat_popup"))
add(require("modules/exp_scenario/control/custom_start"))
add(require("modules/exp_scenario/control/damage_popups"))
add(require("modules/exp_scenario/control/death_markers"))
add(require("modules/exp_scenario/control/deconstruction_log"))
add(require("modules/exp_scenario/control/degrading_tiles"))
add(require("modules/exp_scenario/control/discord_alerts"))
add(require("modules/exp_scenario/control/features"))
add(require("modules/exp_scenario/control/extra_logging"))
add(require("modules/exp_scenario/control/fast_deconstruction"))
add(require("modules/exp_scenario/control/help_bubbles"))
@@ -58,6 +57,7 @@ add(require("modules/exp_scenario/control/inventory_clear"))
add(require("modules/exp_scenario/control/mine_depletion"))
add(require("modules/exp_scenario/control/nuke_protection"))
add(require("modules/exp_scenario/control/pollution_grading"))
add(require("modules/exp_scenario/control/popups"))
add(require("modules/exp_scenario/control/protection"))
add(require("modules/exp_scenario/control/protection_jail"))
add(require("modules/exp_scenario/control/report_jail"))
+27 -8
View File
@@ -3,8 +3,19 @@ Kicks players when all players on the server are afk
]]
local Async = require("modules/exp_util/async")
local Feature = require("modules/exp_scenario/features")
local Roles = require("modules/exp_roles")
local Storage = require("modules/exp_util/storage")
local config = require("modules.exp_legacy.config.afk_kick")
local feature, config = Feature.register("afk_kick", {
admin_as_active = true, -- When true admins will be treated as active regardless of afk time
trust_as_active = true, -- When true trusted players (by playtime) will be treated as active regardless of afk time
active_role_id = Feature.optional("number"), -- Players with this role or higher are treated as active regardless of afk time
afk_minutes = 10, -- The time in minutes that must pass for a player to be considered afk
kick_minutes = 30, -- The time in minutes that must pass without any active players for all players to be kicked
trust_minutes = 600, -- The time in minutes that a player must be online for to count as trusted
})
local ticks_per_minute = 3600
--- @type { last_active: number }
local script_data = { last_active = 0 }
@@ -15,17 +26,25 @@ end)
--- Kicks an afk player, used to add a delay so the gui has time to appear
local afk_kick_player_async =
Async.register(function(player)
if game.tick - script_data.last_active < config.kick_time then return end
if game.tick - script_data.last_active < config.kick_minutes * ticks_per_minute then return end
game.kick_player(player, "AFK while no active players on the server")
end)
--- Check if a player has a role which counts as active regardless of afk time
--- @param player LuaPlayer
--- @return boolean
local function is_active_role(player)
local role = config.active_role_id and Roles.get_role(config.active_role_id)
return role ~= nil and not Roles.get_player_highest_role(player):is_lower_than(role)
end
--- Check if there is an active player
local function has_active_player()
for _, player in ipairs(game.connected_players) do
if player.afk_time < config.afk_time
if player.afk_time < config.afk_minutes * ticks_per_minute
or config.admin_as_active and player.admin
or config.trust_as_active and player.online_time > config.trust_time
or config.custom_active_check and config.custom_active_check(player) then
or config.trust_as_active and player.online_time > config.trust_minutes * ticks_per_minute
or is_active_role(player) then
script_data.last_active = game.tick
return true
end
@@ -40,7 +59,7 @@ local function check_afk_players()
if has_active_player() then return end
-- Check if players should be kicked
if game.tick - script_data.last_active < config.kick_time then return end
if game.tick - script_data.last_active < config.kick_minutes * ticks_per_minute then return end
-- Kick time exceeded, kick all players
for _, player in ipairs(game.connected_players) do
@@ -73,12 +92,12 @@ end
local e = defines.events
return {
return feature:guard{
events = {
[e.on_player_joined_game] = on_player_joined_game,
},
on_nth_tick = {
[config.update_time] = check_afk_players,
[60 * 60] = check_afk_players,
},
has_active_player = has_active_player,
}
+70 -42
View File
@@ -1,42 +1,70 @@
--[[ Control - Bonus
Various bonus related event handlers
TODO Refactor this fully, this is temp to get it out of the player bonus gui file
]]
local Roles = require("modules/exp_roles")
local config = require("modules/exp_legacy/config/bonus")
--- @param event EventData.on_force_created
local function apply_force_bonus(event)
local force = event.force
for k, v in pairs(config.force_bonus) do
force[k] = math.floor(v.max_value / 2)
end
end
--- @param event EventData.on_surface_created
local function apply_surface_bonus(event)
local surface = assert(game.get_surface(event.surface_index))
for k, v in pairs(config.surface_bonus) do
surface[k] = math.floor(v.max_value / 2)
end
end
--- @param event EventData.on_player_died
local function fast_respawn(event)
local player = assert(game.get_player(event.player_index))
if Roles.player_has_permission(player, "exp_scenario.player.instant_respawn") then
player.ticks_to_respawn = 120
end
end
local e = defines.events
return {
events = {
[e.on_force_created] = apply_force_bonus,
[e.on_surface_created] = apply_surface_bonus,
[e.on_player_died] = fast_respawn,
}
}
--[[ Control - Bonus
Various bonus related event handlers
TODO Refactor this fully, this is temp to get it out of the player bonus gui file
]]
local Roles = require("modules/exp_roles")
--- Force bonuses are started at half their max value
local force_bonus = {
["worker_robots_battery_modifier"] = {
max_value = 1,
value_step = 1,
scale = 1,
cost = 1,
is_percentage = false,
},
["worker_robots_storage_bonus"] = {
max_value = 1,
value_step = 1,
scale = 1,
cost = 1,
is_percentage = false,
},
["following_robots_lifetime_modifier"] = {
max_value = 1,
value_step = 1,
scale = 1,
cost = 1,
is_percentage = false,
},
}
--- Surface bonuses are started at half their max value
local surface_bonus = {}
--- @param event EventData.on_force_created
local function apply_force_bonus(event)
local force = event.force
for k, v in pairs(force_bonus) do
force[k] = math.floor(v.max_value / 2)
end
end
--- @param event EventData.on_surface_created
local function apply_surface_bonus(event)
local surface = assert(game.get_surface(event.surface_index))
for k, v in pairs(surface_bonus) do
surface[k] = math.floor(v.max_value / 2)
end
end
--- @param event EventData.on_player_died
local function fast_respawn(event)
local player = assert(game.get_player(event.player_index))
if Roles.player_has_permission(player, "exp_scenario.player.instant_respawn") then
player.ticks_to_respawn = 120
end
end
local e = defines.events
return {
events = {
[e.on_force_created] = apply_force_bonus,
[e.on_surface_created] = apply_surface_bonus,
[e.on_player_died] = fast_respawn,
}
}
@@ -2,10 +2,10 @@
Adds auto replies to chat messages, as well as chat commands
]]
local Feature = require("modules/exp_scenario/features")
local Roles = require("modules/exp_roles")
local config = require("modules.exp_legacy.config.chat_reply")
local prefix = config.command_prefix
local prefix_len = string.len(prefix)
local feature = require("modules/exp_scenario/config/chat_auto_reply")
local config = feature.config
local find = string.find
local sub = string.sub
@@ -16,11 +16,12 @@ local function on_console_chat(event)
if not event.player_index then return end
local player = assert(game.get_player(event.player_index))
local message = event.message:lower():gsub("%s+", "")
local prefix = config.command_prefix
local prefix_len = string.len(prefix)
-- Check if the player can chat commands
local commands_allowed = true
if config.command_admin_only and not player.admin then commands_allowed = false end
if config.command_permission and not Roles.player_has_permission(player, config.command_permission) then commands_allowed = false end
if not Roles.player_has_permission(player, "exp_scenario.chat.commands") then commands_allowed = false end
-- Check if a key word appears in the message
for key_word, reply in pairs(config.messages) do
@@ -56,8 +57,8 @@ end
local e = defines.events
return {
return feature:guard{
events = {
[e.on_console_chat] = on_console_chat,
}
},
}
@@ -1,48 +0,0 @@
--[[-- Control - Chat Popup
Creates flying text entities when a player sends a message in chat
]]
local FlyingText = require("modules/exp_util/flying_text")
local config = require("modules.exp_legacy.config.popup_messages")
local lower = string.lower
local find = string.find
--- Create a chat bubble when a player types a message
--- @param event EventData.on_console_chat
local function on_console_chat(event)
if not event.player_index then return end
local player = assert(game.get_player(event.player_index))
local name = player.name
-- Sends the message as text above them
if config.show_player_messages then
FlyingText.create_as_player{
target_player = player,
text = { "exp_chat-popup.flying-text-message", name, event.message },
}
end
if not config.show_player_mentions then return end
-- Loops over online players to see if they name is included
local search_string = lower(event.message)
for _, mentioned_player in ipairs(game.connected_players) do
if mentioned_player.index ~= player.index then
if find(search_string, lower(mentioned_player.name), 1, true) then
FlyingText.create_as_player{
target_player = mentioned_player,
text = { "exp_chat-popup.flying-text-ping", name },
}
end
end
end
end
local e = defines.events
return {
events = {
[e.on_console_chat] = on_console_chat,
}
}
+14 -3
View File
@@ -2,9 +2,20 @@
Changes the starting script and the items given on first join depending on factory production levels
]]
local config = require("modules.exp_legacy.config.advanced_start")
local Feature = require("modules/exp_scenario/features")
local starting_items = require("modules/exp_scenario/config/starting_items")
local floor = math.floor
--- Applied when the map is created
local feature, config = Feature.register("custom_start", {
skip_intro = true, -- skips the intro given in the default factorio free play scenario
skip_victory = true, -- will skip the victory screen when a rocket is launched
friendly_fire = false, -- weather players will be able to attack each other on the same force
disable_crashsite = true, -- weather to disable creation of the crashsite
enemy_expansion = false, -- a catch all for in case the map settings file fails to load
chart_radius = 320, -- the number of tiles that will be charted when the map starts
})
--- Give a player their starting items
--- @param player LuaPlayer
local function give_starting_items(player)
@@ -12,7 +23,7 @@ local function give_starting_items(player)
local get_input_count = get_prod_stats.get_input_count
local insert_param = { name = "", count = 0 }
local insert = player.insert
for item_name, insert_amount in pairs(config.items) do
for item_name, insert_amount in pairs(starting_items) do
insert_param.name = item_name
if type(insert_amount) == "function" then
local count = insert_amount(get_input_count(item_name), get_input_count, player)
@@ -55,7 +66,7 @@ end
local e = defines.events
return {
return feature:guard{
on_init = on_init,
events = {
[e.on_player_created] = on_player_created,
@@ -1,51 +0,0 @@
--[[-- Control - Damage PopUps
Displays the amount of dmg that is done by players to entities;
also shows player health when a player is attacked
]]
local FlyingText = require("modules/exp_util/flying_text")
local config = require("modules.exp_legacy.config.popup_messages")
local random = math.random
local floor = math.floor
local max = math.max
--- Called when entity entity is damaged including the player character
--- @param event EventData.on_entity_damaged
local function on_entity_damaged(event)
local message
local cause = event.cause
local entity = event.entity
-- Check which message to display
if config.show_player_health and entity.name == "character" then
message = { "exp_damage-popup.flying-text-health", floor(entity.health) }
elseif config.show_player_damage and entity.name ~= "character" and cause and cause.name == "character" then
message = { "exp_damage-popup.flying-text-damage", floor(event.original_damage_amount) }
end
-- Outputs the message as floating text
if message then
local entity_position = entity.position
local entity_radius = max(1, entity.get_radius())
local offset = (random() - 0.5) * entity_radius * config.damage_location_variance
local position = { x = entity_position.x + offset, y = entity_position.y - entity_radius }
local health_percentage = assert(entity.get_health_ratio())
local color = { r = 1 - health_percentage, g = health_percentage, b = 0 }
FlyingText.create{
text = message,
position = position,
color = color,
}
end
end
local e = defines.events
return {
events = {
[e.on_entity_damaged] = on_entity_damaged,
},
}
+24 -21
View File
@@ -3,8 +3,18 @@ Makes markers on the map where places have died and reclaims items if not recove
]]
local ExpUtil = require("modules/exp_util")
local Feature = require("modules/exp_scenario/features")
local Storage = require("modules/exp_util/storage")
local config = require("modules.exp_legacy.config.death_logger")
local feature, config = Feature.register("death_markers", {
collect_corpses = true, -- enables items being returned to the spawn point in chests upon corpse expiring
show_map_markers = true, -- shows markers on the map where bodies are
clean_map_markers = false, -- removes the map marker once the body is gone
include_time_of_death = true, -- weather to include the time of death on the map marker
map_icon = nil, -- the icon that the map marker shows; nil means no icon; format as a SingleID
show_light_at_corpse = true, -- if a light should be rendered at the corpse
show_line_to_corpse = true, -- if a line should be rendered from you to your corpse
})
local map_tag_time_format = ExpUtil.format_time_factory{ format = "short", hours = true, minutes = true }
@@ -40,6 +50,7 @@ end
--- Checks that all map tags are present and valid, creating any that are missing
local function check_map_tags()
if not config.show_map_markers then return end
for _, corpse_data in pairs(character_corpses) do
if not corpse_data.tag or not corpse_data.tag.valid then
create_map_tag(corpse_data)
@@ -98,6 +109,7 @@ end
--- Draw lines to the player corpse
--- @param event EventData.on_player_respawned
local function on_player_respawned(event)
if not config.show_line_to_corpse then return end
local index = event.player_index
local player = assert(game.get_player(index))
for _, corpse_data in pairs(character_corpses) do
@@ -122,6 +134,7 @@ end
--- Collect all items from expired character corpses
--- @param event EventData.on_character_corpse_expired
local function on_character_corpse_expired(event)
if not config.collect_corpses then return end
local corpse = event.corpse
local inventory = assert(corpse.get_inventory(defines.inventory.character_corpse))
ExpUtil.transfer_inventory_to_surface{
@@ -132,26 +145,16 @@ local function on_character_corpse_expired(event)
}
end
local on_nth_tick = {}
if config.show_map_markers then
on_nth_tick[config.period_check_map_tags] = check_map_tags
end
local e = defines.events
local events = {
[e.on_player_died] = on_player_died,
[e.on_object_destroyed] = on_object_destroyed,
}
if config.show_line_to_corpse then
events[e.on_player_respawned] = on_player_respawned
end
if config.collect_corpses then
events[e.on_character_corpse_expired] = on_character_corpse_expired
end
return {
on_nth_tick = on_nth_tick,
events = events,
return feature:guard{
on_nth_tick = {
[60 * 60 * 5] = check_map_tags,
},
events = {
[e.on_player_died] = on_player_died,
[e.on_object_destroyed] = on_object_destroyed,
[e.on_player_respawned] = on_player_respawned,
[e.on_character_corpse_expired] = on_character_corpse_expired,
},
}
@@ -5,7 +5,16 @@ Log certain actions into a file when events are triggered
local ExpUtil = require("modules/exp_util")
local Storage = require("modules/exp_util/storage")
local Roles = require("modules/exp_roles")
local config = require("modules.exp_legacy.config.deconlog")
local Feature = require("modules/exp_scenario/features")
local feature, config = Feature.register("deconstruction_log", {
decon_area = true, -- log when an area is deconstructed
built_entity = true, -- log when an entity is built
mined_entity = true, -- log when an entity is mined
fired_rocket = true, -- log when a rocket is fired
fired_explosive_rocket = true, -- log when an explosive rocket is fired
fired_nuke = true, -- log when a nuke is fired
})
local seconds_time_format = ExpUtil.format_time_factory{ format = "short", hours = true, minutes = true, seconds = true }
local format_number = require("util").format_number
@@ -90,6 +99,7 @@ end
--- Log when an area is deconstructed
--- @param event EventData.on_player_deconstructed_area
local function on_player_deconstructed_area(event)
if not config.decon_area then return end
local player = get_log_player(event)
if not player then return end
@@ -118,6 +128,7 @@ end
--- Log when an entity is built
--- @param event EventData.on_built_entity
local function on_built_entity(event)
if not config.built_entity then return end
local player = get_log_player(event)
if not player then return end
add_log_line(player, "built_entity", format_entity(event.entity))
@@ -126,16 +137,17 @@ end
--- Log when an entity is mined
--- @param event EventData.on_player_mined_entity
local function on_player_mined_entity(event)
if not config.mined_entity then return end
local player = get_log_player(event)
if not player then return end
add_log_line(player, "mined_entity", format_entity(event.entity))
end
--- Ammo which is logged when fired
local logged_ammo = {
["rocket"] = config.fired_rocket,
["explosive-rocket"] = config.fired_explosive_rocket,
["atomic-bomb"] = config.fired_nuke,
--- The setting which decides if a shot of an ammo is logged
local ammo_settings = {
["rocket"] = "fired_rocket",
["explosive-rocket"] = "fired_explosive_rocket",
["atomic-bomb"] = "fired_nuke",
}
--- @class ExpScenario_DeconstructionLog.AmmoSlot
@@ -186,7 +198,7 @@ local function on_player_ammo_inventory_changed(event)
fired = previous.name
end
if fired and logged_ammo[fired] then
if fired and config[ammo_settings[fired]] then
add_log_line(player, "shot-" .. fired, format_position(player.physical_position), format_position(player.shooting_state.position))
end
end
@@ -206,29 +218,16 @@ local function on_player_left_game(event)
end
local e = defines.events
local events = {
[e.on_multiplayer_init] = clear_log,
}
if config.decon_area then
events[e.on_player_deconstructed_area] = on_player_deconstructed_area
end
if config.built_entity then
events[e.on_built_entity] = on_built_entity
end
if config.mined_entity then
events[e.on_player_mined_entity] = on_player_mined_entity
end
if config.fired_rocket or config.fired_explosive_rocket or config.fired_nuke then
events[e.on_player_ammo_inventory_changed] = on_player_ammo_inventory_changed
events[e.on_player_joined_game] = on_player_character_changed
events[e.on_player_respawned] = on_player_character_changed
events[e.on_player_left_game] = on_player_left_game
end
return {
events = events,
return feature:guard{
events = {
[e.on_multiplayer_init] = clear_log,
[e.on_player_deconstructed_area] = on_player_deconstructed_area,
[e.on_built_entity] = on_built_entity,
[e.on_player_mined_entity] = on_player_mined_entity,
[e.on_player_ammo_inventory_changed] = on_player_ammo_inventory_changed,
[e.on_player_joined_game] = on_player_character_changed,
[e.on_player_respawned] = on_player_character_changed,
[e.on_player_left_game] = on_player_left_game,
},
}
+102 -7
View File
@@ -2,13 +2,108 @@
When a player walks around the tiles under them will degrade over time, the same is true when entites are built
]]
local config = require("modules.exp_legacy.config.scorched_earth")
local Feature = require("modules/exp_scenario/features")
local feature, config = Feature.register("degrading_tiles", {
weakness_value = 70, -- lower value will make tiles more likely to degrade
entities = Feature.set{ -- entities which degrade the tiles under them when they are placed
"stone-furnace", "steel-furnace", "electric-furnace", "assembling-machine-1", "assembling-machine-2", "assembling-machine-3",
"beacon", "centrifuge", "chemical-plant", "oil-refinery", "storage-tank", "nuclear-reactor",
"steam-engine", "steam-turbine", "boiler", "heat-exchanger", "stone-wall", "gate",
"gun-turret", "laser-turret", "flamethrower-turret", "radar", "lab", "big-electric-pole",
"substation", "rocket-silo", "pumpjack", "electric-mining-drill", "roboport", "accumulator",
},
})
local random = math.random
--- How "strong" a tile is, bigger number means less likely to degrade
local strengths = {
-- debug: /interface require('modules.addons.worn-paths')(player.name,true)
-- note: tiles are effected by the tiles around them, so player paths will not degrade as fast when made wider
-- note: values are relative to the tile with the highest value, recommended to keep highest tile as a "nice" number
-- note: tiles not in list will never degrade under any conditions (which is why some are omitted such as water)
["refined-concrete"] = 100,
["refined-hazard-concrete-left"] = 100,
["refined-hazard-concrete-right"] = 100,
["concrete"] = 90,
["hazard-concrete-left"] = 90,
["hazard-concrete-right"] = 90,
["stone-path"] = 80,
["red-desert-0"] = 80,
["dry-dirt"] = 50,
-- grass four (main grass tiles)
["grass-1"] = 50,
["grass-2"] = 40,
["grass-3"] = 30,
["grass-4"] = 25,
-- red three (main red tiles)
["red-desert-1"] = 40,
["red-desert-2"] = 30,
["red-desert-3"] = 25,
-- sand three (main sand tiles)
["sand-1"] = 40,
["sand-2"] = 30,
["sand-3"] = 25,
-- dirt 3 (main dirt tiles)
["dirt-1"] = 40,
["dirt-2"] = 30,
["dirt-3"] = 25,
-- last three/four (all sets of three merge here)
["dirt-4"] = 25,
["dirt-5"] = 30,
["dirt-6"] = 40,
-- ["dirt-7"]=0, -- last tile, nothing to degrade to
-- land fill chain
-- ["landfill"]=50,
-- ["water-shallow"]=90,
-- ["water-mud"]=0, -- last tile, nothing to degrade to
}
--- When a tile degrades it will turn into the next tile given here
local degrade_order = {
["refined-concrete"] = "concrete",
["refined-hazard-concrete-left"] = "hazard-concrete-left",
["refined-hazard-concrete-right"] = "hazard-concrete-right",
["concrete"] = "stone-path",
["hazard-concrete-left"] = "stone-path",
["hazard-concrete-right"] = "stone-path",
["stone-path"] = "dry-dirt",
["red-desert-0"] = "dry-dirt",
["dry-dirt"] = "dirt-4",
-- grass four (main grass tiles)
["grass-1"] = "grass-2",
["grass-2"] = "grass-3",
["grass-3"] = "grass-4",
["grass-4"] = "dirt-4",
-- red three (main red tiles)
["red-desert-1"] = "red-desert-2",
["red-desert-2"] = "red-desert-3",
["red-desert-3"] = "dirt-4",
-- sand three (main sand tiles)
["sand-1"] = "sand-2",
["sand-2"] = "sand-3",
["sand-3"] = "dirt-4",
-- dirt 3 (main dirt tiles)
["dirt-1"] = "dirt-2",
["dirt-2"] = "dirt-3",
["dirt-3"] = "dirt-4",
-- last three/four (all sets of three merge here)
["dirt-4"] = "dirt-5",
["dirt-5"] = "dirt-6",
["dirt-6"] = "dirt-7",
-- ["dirt-7"]=0, -- last tile, nothing to degrade to
-- land fill chain
-- ["landfill"]='grass-2', -- 'water-shallow'
-- ["water-shallow"]='water-mud',
-- ["water-mud"]=0, -- last tile, nothing to degrade to
}
--- Get the max tile strength
local max_strength = 0
for _, strength in pairs(config.strengths) do
for _, strength in pairs(strengths) do
if strength > max_strength then
max_strength = strength
end
@@ -20,7 +115,7 @@ end
local function degrade_tile(surface, position)
local tile = surface.get_tile(position.x, position.y)
local tile_name = tile.name
local degrade_tile_name = config.degrade_order[tile_name]
local degrade_tile_name = degrade_order[tile_name]
if not degrade_tile_name then return end
surface.set_tiles{ { name = degrade_tile_name, position = position } }
end
@@ -39,7 +134,7 @@ local function degrade_entity(entity)
for y = left_top.y, right_bottom.y do
local tile = surface.get_tile(x, y)
local tile_name = tile.name
local degrade_tile_name = config.degrade_order[tile_name]
local degrade_tile_name = degrade_order[tile_name]
if degrade_tile_name then
tiles[#tiles + 1] = { name = degrade_tile_name, position = { x, y } }
end
@@ -63,14 +158,14 @@ end
local function get_tile_strength(surface, position)
local tile = surface.get_tile(position.x, position.y)
local tile_name = tile.name
local strength = config.strengths[tile_name]
local strength = strengths[tile_name]
if not strength then return end
for x = position.x - 1, position.x + 1 do
for y = position.y - 1, position.y + 1 do
local check_tile = surface.get_tile(x, y)
local check_tile_name = check_tile.name
local check_strength = config.strengths[check_tile_name] or 0
local check_strength = strengths[check_tile_name] or 0
strength = strength + check_strength
end
end
@@ -108,7 +203,7 @@ end
local e = defines.events
return {
return feature:guard{
events = {
[e.on_player_changed_position] = on_player_changed_position,
[e.on_robot_built_entity] = on_built_entity,
+154 -141
View File
@@ -4,7 +4,34 @@ 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 config = require("modules.exp_legacy.config.discord_alerts")
local Feature = require("modules/exp_scenario/features")
local EntityProtection = require("modules/exp_scenario/control/protection")
local Jail = require("modules/exp_scenario/control/jail")
local feature, config = Feature.register("discord_alerts", {
show_playtime = true, -- Add the playtime of players to alerts
entity_protection = true, -- Alert when a player repeatedly removes protected entities
player_bans = true, -- Alert when a player is banned or unbanned
player_mutes = true, -- Alert when a player is muted or unmuted
player_kicks = true, -- Alert when a player is kicked
player_promotes = false, -- Alert when a player is promoted or demoted
player_jail = true, -- Alert when a player is jailed or unjailed
logged_commands = Feature.set{ -- Alert when a player uses one of these commands
"config", "purge", "c", "command", "silent-command", "measured-command", "banlist", "permissions", "editor", "cheat",
},
})
--- Wrap an event handler so it only runs while the alert is enabled
--- @param setting string
--- @param handler fun(event: table)
--- @return fun(event: table)
local function when(setting, handler)
return function(event)
if config[setting] then
return handler(event)
end
end
end
local format_string = string.format
local write_json = ExpUtil.write_json
@@ -80,172 +107,158 @@ local function emit_event(opts)
end
--- Repeated protected entity mining
if config.entity_protection then
local EntityProtection = require("modules/exp_scenario/control/protection")
events[EntityProtection.on_repeat_violation] = function(event)
local player_name = get_player_name(event)
emit_event{
title = "Entity Protection",
description = "A player removed protected entities",
color = Colors.yellow,
fields = {
{ name = "Player", inline = true, value = append_playtime(player_name) },
{ name = "Entity", inline = true, value = event.entity.name },
{ name = "Location", value = format_string("X %.1f Y %.1f", event.entity.position.x, event.entity.position.y) },
},
}
end
end
events[EntityProtection.on_repeat_violation] = when("entity_protection", function(event)
local player_name = get_player_name(event)
emit_event{
title = "Entity Protection",
description = "A player removed protected entities",
color = Colors.yellow,
fields = {
{ name = "Player", inline = true, value = append_playtime(player_name) },
{ name = "Entity", inline = true, value = event.entity.name },
{ name = "Location", value = format_string("X %.1f Y %.1f", event.entity.position.x, event.entity.position.y) },
},
}
end)
--- When a player is jailed or unjailed
if config.player_jail then
local Jail = require("modules/exp_scenario/control/jail")
events[Jail.on_player_jailed] = function(event)
local player_name, by_player_name = get_player_name(event)
events[Jail.on_player_jailed] = when("player_jail", function(event)
local player_name, by_player_name = get_player_name(event)
emit_event{
title = "Jail",
description = "A player has been jailed",
color = Colors.yellow,
fields = {
{ name = "Player", inline = true, value = append_playtime(player_name) },
{ name = "By", inline = true, value = append_playtime(by_player_name) },
{ name = "Reason", value = event.reason },
},
}
end)
events[Jail.on_player_unjailed] = when("player_jail", function(event)
local player_name, by_player_name = get_player_name(event)
emit_event{
title = "Unjail",
description = "A player has been unjailed",
color = Colors.green,
fields = {
{ name = "Player", inline = true, value = append_playtime(player_name) },
{ name = "By", inline = true, value = append_playtime(by_player_name) },
},
}
end)
--- Ban and unban
--- @param event EventData.on_player_banned
events[e.on_player_banned] = when("player_bans", function(event)
if event.by_player then
local by_player = game.players[event.by_player]
emit_event{
title = "Jail",
description = "A player has been jailed",
color = Colors.yellow,
title = "Banned",
description = "A player has been banned",
color = Colors.red,
fields = {
{ name = "Player", inline = true, value = append_playtime(player_name) },
{ name = "By", inline = true, value = append_playtime(by_player_name) },
{ name = "Player", inline = true, value = append_playtime(event.player_name) },
{ name = "By", inline = true, value = append_playtime(by_player.name) },
{ name = "Reason", value = event.reason },
},
}
end
events[Jail.on_player_unjailed] = function(event)
local player_name, by_player_name = get_player_name(event)
end)
--- @param event EventData.on_player_unbanned
events[e.on_player_unbanned] = when("player_bans", function(event)
if event.by_player then
local by_player = game.players[event.by_player]
emit_event{
title = "Unjail",
description = "A player has been unjailed",
title = "Un-Banned",
description = "A player has been un-banned",
color = Colors.green,
fields = {
{ name = "Player", inline = true, value = append_playtime(player_name) },
{ name = "By", inline = true, value = append_playtime(by_player_name) },
{ name = "Player", inline = true, value = append_playtime(event.player_name) },
{ name = "By", inline = true, value = append_playtime(by_player.name) },
{ name = "Reason", value = event.reason },
},
}
end
end
--- Ban and unban
if config.player_bans then
--- @param event EventData.on_player_banned
events[e.on_player_banned] = function(event)
if event.by_player then
local by_player = game.players[event.by_player]
emit_event{
title = "Banned",
description = "A player has been banned",
color = Colors.red,
fields = {
{ name = "Player", inline = true, value = append_playtime(event.player_name) },
{ name = "By", inline = true, value = append_playtime(by_player.name) },
{ name = "Reason", value = event.reason },
},
}
end
end
--- @param event EventData.on_player_unbanned
events[e.on_player_unbanned] = function(event)
if event.by_player then
local by_player = game.players[event.by_player]
emit_event{
title = "Un-Banned",
description = "A player has been un-banned",
color = Colors.green,
fields = {
{ name = "Player", inline = true, value = append_playtime(event.player_name) },
{ name = "By", inline = true, value = append_playtime(by_player.name) },
{ name = "Reason", value = event.reason },
},
}
end
end
end
end)
--- Mute and unmute
if config.player_mutes then
--- @param event EventData.on_player_muted
events[e.on_player_muted] = function(event)
local player_name = get_player_name(event)
emit_event{
title = "Muted",
description = "A player has been muted",
color = Colors.yellow,
fields = {
{ name = "Player", inline = true, value = append_playtime(player_name) },
},
}
end
--- @param event EventData.on_player_unmuted
events[e.on_player_unmuted] = function(event)
local player_name = get_player_name(event)
emit_event{
title = "Un-Muted",
description = "A player has been un-muted",
color = Colors.green,
fields = {
{ name = "Player", inline = true, value = append_playtime(player_name) },
},
}
end
end
--- @param event EventData.on_player_muted
events[e.on_player_muted] = when("player_mutes", function(event)
local player_name = get_player_name(event)
emit_event{
title = "Muted",
description = "A player has been muted",
color = Colors.yellow,
fields = {
{ name = "Player", inline = true, value = append_playtime(player_name) },
},
}
end)
--- @param event EventData.on_player_unmuted
events[e.on_player_unmuted] = when("player_mutes", function(event)
local player_name = get_player_name(event)
emit_event{
title = "Un-Muted",
description = "A player has been un-muted",
color = Colors.green,
fields = {
{ name = "Player", inline = true, value = append_playtime(player_name) },
},
}
end)
--- Kick
if config.player_kicks then
--- @param event EventData.on_player_kicked
events[e.on_player_kicked] = function(event)
if event.by_player then
local player_name = get_player_name(event)
local by_player = game.players[event.by_player]
emit_event{
title = "Kick",
description = "A player has been kicked",
color = Colors.orange,
fields = {
{ name = "Player", inline = true, value = append_playtime(player_name) },
{ name = "By", inline = true, value = append_playtime(by_player.name) },
{ name = "Reason", value = event.reason },
},
}
end
--- @param event EventData.on_player_kicked
events[e.on_player_kicked] = when("player_kicks", function(event)
if event.by_player then
local player_name = get_player_name(event)
local by_player = game.players[event.by_player]
emit_event{
title = "Kick",
description = "A player has been kicked",
color = Colors.orange,
fields = {
{ name = "Player", inline = true, value = append_playtime(player_name) },
{ name = "By", inline = true, value = append_playtime(by_player.name) },
{ name = "Reason", value = event.reason },
},
}
end
end
end)
--- Promote and demote
if config.player_promotes then
--- @param event EventData.on_player_promoted
events[e.on_player_promoted] = function(event)
local player_name = get_player_name(event)
emit_event{
title = "Promote",
description = "A player has been promoted",
color = Colors.green,
fields = {
{ name = "Player", inline = true, value = append_playtime(player_name) },
},
}
end
--- @param event EventData.on_player_demoted
events[e.on_player_demoted] = function(event)
local player_name = get_player_name(event)
emit_event{
title = "Demote",
description = "A player has been demoted",
color = Colors.yellow,
fields = {
{ name = "Player", inline = true, value = append_playtime(player_name) },
},
}
end
end
--- @param event EventData.on_player_promoted
events[e.on_player_promoted] = when("player_promotes", function(event)
local player_name = get_player_name(event)
emit_event{
title = "Promote",
description = "A player has been promoted",
color = Colors.green,
fields = {
{ name = "Player", inline = true, value = append_playtime(player_name) },
},
}
end)
--- @param event EventData.on_player_demoted
events[e.on_player_demoted] = when("player_promotes", function(event)
local player_name = get_player_name(event)
emit_event{
title = "Demote",
description = "A player has been demoted",
color = Colors.yellow,
fields = {
{ name = "Player", inline = true, value = append_playtime(player_name) },
},
}
end)
--- @param event EventData.on_console_command
events[e.on_console_command] = function(event)
if event.player_index then
local player_name = get_player_name(event)
if config[event.command] then
if config.logged_commands[event.command] then
emit_event{
title = event.command:gsub("^%l", string.upper),
description = "/" .. event.command .. " was used",
@@ -259,6 +272,6 @@ events[e.on_console_command] = function(event)
end
end
return {
return feature:guard{
events = events,
}
+38 -7
View File
@@ -2,8 +2,39 @@
Log some extra events to a separate file
]]
local config = require("modules.exp_legacy.config.logging")
local config_res = require("modules.exp_legacy.config.research")
local Feature = require("modules/exp_scenario/features")
local research_data = require("modules/exp_scenario/config/research_data")
local feature, config = Feature.register("extra_logging", {
file_name = "log/logging.log", -- the file the lines are written to
rocket_launch_display_rate = 500, -- after the counts below, log every this many rockets
})
--- Rocket counts which are always logged
local rocket_launch_display = {
[1] = true,
[2] = true,
[5] = true,
[10] = true,
[20] = true,
[50] = true,
[100] = true,
[200] = true,
}
local disconnect_reason = {
[defines.disconnect_reason.quit] = "left the game",
[defines.disconnect_reason.dropped] = "was dropped from the game",
[defines.disconnect_reason.reconnect] = "is reconnecting",
[defines.disconnect_reason.wrong_input] = "was having a wrong input",
[defines.disconnect_reason.desync_limit_reached] = "had desync limit reached",
[defines.disconnect_reason.cannot_keep_up] = "cannot keep up",
[defines.disconnect_reason.afk] = "was afk",
[defines.disconnect_reason.kicked] = "was kicked",
[defines.disconnect_reason.kicked_and_deleted] = "was kicked and deleted",
[defines.disconnect_reason.banned] = "was banned",
[defines.disconnect_reason.switching_servers] = "is switching servers",
}
local concat = table.concat
local write_file = helpers.write_file
@@ -26,7 +57,7 @@ local function on_cargo_pod_finished_ascending(event)
local force = event.cargo_pod.force --[[@as LuaForce]]
if force.rockets_launched >= config.rocket_launch_display_rate and force.rockets_launched % config.rocket_launch_display_rate == 0 then
add_log_line("[ROCKET]", force.rockets_launched, "rockets launched")
elseif config.rocket_launch_display[force.rockets_launched] then
elseif rocket_launch_display[force.rockets_launched] then
add_log_line("[ROCKET]", force.rockets_launched, "rockets launched")
end
end
@@ -53,7 +84,7 @@ local function on_research_finished(event)
return
end
local inf_research_level = config_res.inf_res[config_res.mod_set][event.research.name]
local inf_research_level = research_data.inf_res[research_data.mod_set][event.research.name]
if inf_research_level and event.research.level >= inf_research_level then
add_log_line_locale{ "", "[RES]", event.research.prototype.localised_name, " at level ", event.research.level - 1, " has been researched\n" }
else
@@ -70,17 +101,17 @@ end
--- @param event EventData.on_player_left_game
local function on_player_left_game(event)
local player = assert(game.get_player(event.player_index))
add_log_line("[LEAVE]", player.name, config.disconnect_reason[event.reason])
add_log_line("[LEAVE]", player.name, disconnect_reason[event.reason])
end
local e = defines.events
return {
return feature:guard{
events = {
[e.on_cargo_pod_finished_ascending] = on_cargo_pod_finished_ascending,
[e.on_pre_player_died] = on_pre_player_died,
[e.on_research_finished] = on_research_finished,
[e.on_player_joined_game] = on_player_joined_game,
[e.on_player_left_game] = on_player_left_game,
}
},
}
+21
View File
@@ -0,0 +1,21 @@
--[[-- Control - Features
Refreshes the gui of every player when a feature is enabled or disabled, see features.lua
]]
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
--- @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
end
return {
events = {
[Feature.events.on_config_updated] = on_config_updated,
},
}
+32 -5
View File
@@ -4,7 +4,34 @@ Adds friendly biters that walk around and give helpful messages
local Async = require("modules/exp_util/async")
local Storage = require("modules/exp_util/storage")
local config = require("modules.exp_legacy.config.compilatron")
local Feature = require("modules/exp_scenario/features")
local config_server_detail = require("modules.exp_legacy.config.server_detail")
local feature = Feature.register("help_bubbles")
--- Ticks between message changes
local message_cycle = 60 * 15
local locations = {
["Spawn"] = {
spawn_position = { x = 0, y = 0 },
spawn_surface = "nauvis",
entity_name = "small-biter",
messages = {
{ "info.website", config_server_detail["website"] },
{ "info.read-readme" },
{ "info.discord", config_server_detail["discord"] },
{ "info.softmod" },
{ "info.redmew" },
{ "info.custom-commands" },
{ "info.status", config_server_detail["status"] },
{ "info.lhd" },
{ "info.github", config_server_detail["github"] },
{ "info.patreon", config_server_detail["patreon"] },
},
}
}
--- @type table<string, Async.AsyncReturn<any>>
local persistent_locations = {}
@@ -48,7 +75,7 @@ local speech_bubble_task =
task.previous_message = entity.surface.create_entity(speech_bubble_param)
task.current_message_index = index + 1
return Async.status.delay(config.message_cycle, task)
return Async.status.delay(message_cycle, task)
end)
--- Register an entity to start spawning speech bubbles
@@ -66,7 +93,7 @@ end
--- Check all persistent locations from the config are active
local function check_persistent_locations()
for name, location in pairs(config.locations) do
for name, location in pairs(locations) do
local task = persistent_locations[name]
if task and not task.completed then
goto continue
@@ -92,9 +119,9 @@ local function check_persistent_locations()
end
end
return {
return feature:guard{
on_nth_tick = {
[config.message_cycle] = check_persistent_locations,
[message_cycle] = check_persistent_locations,
},
register_entity = register_entity,
}
@@ -6,7 +6,15 @@ on_player_kicked - player_index
]]
local ExpUtil = require("modules/exp_util")
local events = require("modules.exp_legacy.config.inventory_clear")
local Feature = require("modules/exp_scenario/features")
local feature = Feature.register("inventory_clear")
--- The events which clear the inventory of the player
local events = {
defines.events.on_player_banned,
defines.events.on_player_kicked,
}
--- @param event { player_index: number }
local function clear_items(event)
@@ -26,6 +34,6 @@ for _, event_name in ipairs(events) do
event_handlers[event_name] = clear_items
end
return {
return feature:guard{
events = event_handlers,
}
@@ -3,7 +3,13 @@ Marks mining drills for deconstruction when resources deplete
]]
local Async = require("modules/exp_util/async")
local config = require("modules.exp_legacy.config.miner")
local Feature = require("modules/exp_scenario/features")
local feature, config = Feature.register("mine_depletion", {
fluid = true, -- When true, checks for for fluid pipes when removing miners
chest = true, -- When true, checks for for chest when removing miners
beacon = true, -- When true, checks for for beacon when removing miners
})
local floor = math.floor
@@ -270,7 +276,7 @@ end
local e = defines.events
return {
return feature:guard{
events = {
[e.on_resource_depleted] = on_resource_depleted,
},
+25 -17
View File
@@ -3,20 +3,32 @@ Disable new players from having certain items in their inventory, most commonly
]]
local ExpUtil = require("modules/exp_util")
local Feature = require("modules/exp_scenario/features")
local Roles = require("modules/exp_roles")
local config = require("modules.exp_legacy.config.nukeprotect")
local feature, config = Feature.register("nuke_protection", {
banned_items = Feature.set{ "atomic-bomb" }, -- Items which are removed from the inventory of players without the bypass permission
})
--- The inventories which are checked and the events which trigger the check
local inventories = {
[defines.events.on_player_ammo_inventory_changed] = defines.inventory.character_ammo,
[defines.events.on_player_armor_inventory_changed] = defines.inventory.character_armor,
[defines.events.on_player_gun_inventory_changed] = defines.inventory.character_guns,
[defines.events.on_player_main_inventory_changed] = defines.inventory.character_main,
}
--- Check all items in the given inventory
--- @param player LuaPlayer
--- @param type defines.inventory
--- @param banned_items string[]
local function check_items(player, type, banned_items)
local function check_items(player, type)
-- If the player has perms to be ignored, then they should be
if config.ignore_permission and Roles.player_has_permission(player, config.ignore_permission) then return end
if config.ignore_admins and player.admin then return end
if Roles.player_has_permission(player, "exp_scenario.bypass.nuke_protection") then return end
local banned_items = config.banned_items
local items = {} --- @type LuaItemStack[]
local inventory = assert(player.get_inventory(type))
local inventory = player.get_inventory(type)
if not inventory then return end
-- Check what items the player has
for i = 1, #inventory do
local item = inventory[i --[[@as uint]]]
@@ -37,20 +49,16 @@ end
--- Add event handlers for the different inventories
local events = {}
for index, inventory in ipairs(config.inventories) do
if next(inventory.items) then
local assert_msg = "invalid event, no player index, index: " .. index
--- @param event { player_index: number }
events[inventory.event] = function(event)
local player_index = assert(event.player_index, assert_msg)
local player = assert(game.get_player(player_index))
if player and player.valid then
check_items(player, inventory.inventory, inventory.items)
end
for event_id, inventory in pairs(inventories) do
--- @param event { player_index: number }
events[event_id] = function(event)
local player = assert(game.get_player(event.player_index))
if player.valid then
check_items(player, inventory)
end
end
end
return {
return feature:guard{
events = events,
}
@@ -2,7 +2,16 @@
Makes pollution look much nice of the map, ie not one big red mess
]]
local config = require("modules.exp_legacy.config.pollution_grading")
local Feature = require("modules/exp_scenario/features")
local feature, config = Feature.register("pollution_grading", {
reference_point = { -- where pollution is read from
x = 0,
y = 0,
},
max_scalar = 0.5, -- the scale between true max and max
min_scalar = 0.17, -- the scale between the lowest max and min
})
local function check_surfaces()
local max_reference = 0
@@ -20,8 +29,8 @@ local function check_surfaces()
settings.min_to_show_per_chunk = min
end
return {
return feature:guard{
on_nth_tick = {
[config.update_delay * 3600] = check_surfaces,
}
[15 * 3600] = check_surfaces,
},
}
+92
View File
@@ -0,0 +1,92 @@
--[[-- Control - Popups
Creates flying text for chat messages and for damage dealt to and by players
]]
local FlyingText = require("modules/exp_util/flying_text")
local Feature = require("modules/exp_scenario/features")
local feature, config = Feature.register("popups", {
show_player_messages = true, -- weather a message in chat will make a popup above them
show_player_mentions = true, -- weather a mentioned player will have a popup when mentioned in chat
show_player_damage = true, -- weather to show damage done by players
show_player_health = true, -- weather to show player health when attacked
damage_location_variance = 0.8, -- how close to the eade of an entity the popups will appear
})
local lower = string.lower
local find = string.find
local random = math.random
local floor = math.floor
local max = math.max
--- Create a chat bubble when a player types a message
--- @param event EventData.on_console_chat
local function on_console_chat(event)
if not event.player_index then return end
local player = assert(game.get_player(event.player_index))
local name = player.name
-- Sends the message as text above them
if config.show_player_messages then
FlyingText.create_as_player{
target_player = player,
text = { "exp_chat-popup.flying-text-message", name, event.message },
}
end
if not config.show_player_mentions then return end
-- Loops over online players to see if they name is included
local search_string = lower(event.message)
for _, mentioned_player in ipairs(game.connected_players) do
if mentioned_player.index ~= player.index then
if find(search_string, lower(mentioned_player.name), 1, true) then
FlyingText.create_as_player{
target_player = mentioned_player,
text = { "exp_chat-popup.flying-text-ping", name },
}
end
end
end
end
--- Called when entity entity is damaged including the player character
--- @param event EventData.on_entity_damaged
local function on_entity_damaged(event)
local message
local cause = event.cause
local entity = event.entity
-- Check which message to display
if config.show_player_health and entity.name == "character" then
message = { "exp_damage-popup.flying-text-health", floor(entity.health) }
elseif config.show_player_damage and entity.name ~= "character" and cause and cause.name == "character" then
message = { "exp_damage-popup.flying-text-damage", floor(event.original_damage_amount) }
end
-- Outputs the message as floating text
if message then
local entity_position = entity.position
local entity_radius = max(1, entity.get_radius())
local offset = (random() - 0.5) * entity_radius * config.damage_location_variance
local position = { x = entity_position.x + offset, y = entity_position.y - entity_radius }
local health_percentage = assert(entity.get_health_ratio())
local color = { r = 1 - health_percentage, g = health_percentage, b = 0 }
FlyingText.create{
text = message,
position = position,
color = color,
}
end
end
local e = defines.events
return feature:guard{
events = {
[e.on_console_chat] = on_console_chat,
[e.on_entity_damaged] = on_entity_damaged,
},
}
+79 -48
View File
@@ -3,8 +3,21 @@ Protects entities and areas from being mined by players other than the one who p
]]
local Storage = require("modules/exp_util/storage")
local Feature = require("modules/exp_scenario/features")
local Roles = require("modules/exp_roles")
local config = require("modules.exp_legacy.config.protection")
local feature, config = Feature.register("protection", {
repeat_count = 5, -- Number of protected entities that must be removed within repeat_minutes in order to trigger repeated removal protection
repeat_minutes = 20, -- The length of time, in minutes, that protected removals will be remembered for
always_protected_names = Feature.set{}, -- Names of entities which are always protected
always_protected_types = Feature.set{ -- Types of entities which are always protected
"boiler", "generator", "offshore-pump", "reactor", "heat-exchanger", "heat-pipe", "fusion-reactor", "fusion-generator", "power-switch", "rocket-silo",
},
always_trigger_repeat_names = Feature.set{}, -- Names of entities which always trigger repeated removal protection
always_trigger_repeat_types = Feature.set{ -- Types of entities which always trigger repeated removal protection
"reactor", "fusion-reactor", "rocket-silo",
},
})
local format_string = string.format
local floor = math.floor
@@ -17,12 +30,8 @@ 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 = config.always_protected_names,
--- Types of entities which are always protected
--- @type string[]
protected_entity_types = config.always_protected_types,
--- The config of the feature, read only
config = config,
--- @package
events = {},
--- @package
@@ -36,33 +45,29 @@ 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
end
--- @class ExpScenario_Protection.Registration
--- @field surface_index uint
--- @field key string
--- @field entity LuaEntity
local always_protected_names = to_set(config.always_protected_names)
local always_protected_types = to_set(config.always_protected_types)
local always_trigger_repeat_names = to_set(config.always_trigger_repeat_names)
local always_trigger_repeat_types = to_set(config.always_trigger_repeat_types)
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
local repeats = {} --- @type table<string, ExpScenario_Protection.Repeat> Keyed by player name
local protected_entities = {} --- @type table<uint, table<string, LuaEntity>> By surface index then entity key
local protected_areas = {} --- @type table<uint, table<string, BoundingBox>> By surface index then area key
local registrations = {} --- @type table<uint64, ExpScenario_Protection.Registration> By on_object_destroyed registration number
local repeats = {} --- @type table<string, ExpScenario_Protection.Repeat> By player name
Storage.register({
protected_entities = protected_entities,
protected_areas = protected_areas,
registrations = registrations,
repeats = repeats,
}, function(tbl)
protected_entities = tbl.protected_entities
protected_areas = tbl.protected_areas
registrations = tbl.registrations
repeats = tbl.repeats
end, function(tbl)
-- Saves from before entities were registered
tbl.registrations = tbl.registrations or {}
end)
--- Get the key an entity is stored under
@@ -79,15 +84,18 @@ function Protection.get_area_key(area)
return format_string("%i,%i", floor(area.left_top.x), floor(area.left_top.y))
end
--- Protect an entity
--- Protect an entity, it is forgotten when destroyed
--- @param entity LuaEntity
function Protection.add_entity(entity)
local entities = protected_entities[entity.surface.index]
local surface_index = entity.surface.index
local entities = protected_entities[surface_index]
if not entities then
entities = {}
protected_entities[entity.surface.index] = entities
protected_entities[surface_index] = entities
end
entities[Protection.get_entity_key(entity)] = entity
local key = Protection.get_entity_key(entity)
entities[key] = entity
registrations[script.register_on_object_destroyed(entity)] = { surface_index = surface_index, key = key, entity = entity }
end
--- Remove the protection from an entity
@@ -95,7 +103,11 @@ end
function Protection.remove_entity(entity)
local entities = protected_entities[entity.surface.index]
if not entities then return end
entities[Protection.get_entity_key(entity)] = nil
local key = Protection.get_entity_key(entity)
if entities[key] == entity then
entities[key] = nil
registrations[script.register_on_object_destroyed(entity)] = nil
end
end
--- Get the protected entities on a surface, always protected entities are not included
@@ -109,7 +121,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 config.always_protected_names[entity.name] or 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
@@ -161,14 +173,13 @@ function Protection.is_position_protected(surface, position)
return false
end
--- Players are never checked against their own entities, and can be excluded by permission or admin status
--- Players are never checked against their own entities, and can be excluded by permission
--- @param player LuaPlayer
--- @param entity LuaEntity
--- @return boolean
local function is_ignored(player, entity)
if config.ignore_admins and player.admin then return true end
if entity.last_user == nil or entity.last_user.index == player.index then return true end
if config.ignore_permission and Roles.player_has_permission(player, config.ignore_permission) then return true end
if Roles.player_has_permission(player, "exp_scenario.bypass.entity_protection") then return true end
return false
end
@@ -187,14 +198,14 @@ 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 = config.always_trigger_repeat_names[entity.name] or 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)
end
end
--- Raise the protection events when a protected entity is mined, then forget the entity
--- Raise the protection events when a protected entity is mined
--- @param event EventData.on_pre_player_mined_item
local function on_pre_player_mined_item(event)
local entity = event.entity
@@ -205,19 +216,41 @@ local function on_pre_player_mined_item(event)
then
raise_violation(event, player)
end
Protection.remove_entity(entity)
end
--- Forget an entity once it no longer exists
--- @param event { entity: LuaEntity }
local function on_entity_removed(event)
Protection.remove_entity(event.entity)
--- Forget a protected entity once it is destroyed, unless another one took its place
--- @param registration_number uint64
--- @param registration ExpScenario_Protection.Registration
local function forget_entity(registration_number, registration)
registrations[registration_number] = nil
local entities = protected_entities[registration.surface_index]
if entities and entities[registration.key] == registration.entity then
entities[registration.key] = nil
end
end
--- @param event EventData.on_object_destroyed
local function on_object_destroyed(event)
local registration = registrations[event.registration_number]
if registration then
forget_entity(event.registration_number, registration)
end
end
--- Entities destroyed while the feature was disabled were not forgotten
--- @param event EventData.ExpScenario.on_config_updated
local function on_config_updated(event)
if event.feature_name ~= feature.name or event.path ~= "enabled" then return end
for registration_number, registration in pairs(registrations) do
if not registration.entity.valid then
forget_entity(registration_number, registration)
end
end
end
--- Forget protected removals older than the repeat lifetime
local function clear_old_repeats()
local old = game.tick - config.repeat_lifetime
local old = game.tick - config.repeat_minutes * 3600
for player_name, player_repeats in pairs(repeats) do
if player_repeats.last <= old then
repeats[player_name] = nil
@@ -228,10 +261,8 @@ end
local e = defines.events
Protection.events[e.on_pre_player_mined_item] = on_pre_player_mined_item
Protection.events[e.on_space_platform_pre_mined] = on_entity_removed
Protection.events[e.on_robot_pre_mined] = on_entity_removed
Protection.events[e.on_entity_died] = on_entity_removed
Protection.events[e.script_raised_destroy] = on_entity_removed
Protection.on_nth_tick[config.refresh_rate] = clear_old_repeats
Protection.events[e.on_object_destroyed] = on_object_destroyed
Protection.events[Feature.events.on_config_updated] = on_config_updated
Protection.on_nth_tick[3600 * 5] = clear_old_repeats
return Protection
return feature:guard(Protection)
+62 -48
View File
@@ -1,48 +1,62 @@
--[[ Control - Research
Various research related event handlers
TODO Refactor this fully, this is temp to get it out of the research times gui file
]]
local config = require("modules/exp_legacy/config/research")
--- @param event EventData.on_research_finished
local function on_research_finished(event)
local research_name = event.research.name
if config.bonus_inventory.enabled and config.bonus_inventory.res[research_name] then
event.research.force[config.bonus_inventory.name] = math.min((event.research.level - 1) * config.bonus_inventory.rate, config.bonus_inventory.limit)
end
if config.pollution_ageing_by_research and config.bonus_inventory.res[research_name] then
game.map_settings.pollution.ageing = math.min(10, event.research.level / 5)
end
end
--- @param event EventData.on_research_started
local function on_research_started(event)
if config.limit_res[event.research.name] and event.research.level > config.limit_res[event.research.name] then
event.research.enabled = false
event.research.visible_when_disabled = true
local rq = event.research.force.research_queue
for i = #rq, 1, -1 do
if rq[i] == event.research.name then
table.remove(rq, i)
end
end
event.research.force.cancel_current_research()
event.research.force.research_queue = rq
end
end
local e = defines.events
return {
events = {
[e.on_research_finished] = on_research_finished,
[e.on_research_reversed] = on_research_finished,
[e.on_research_started] = on_research_started,
[e.on_research_queued] = on_research_started,
}
}
--[[ Control - Research
Various research related event handlers
TODO Refactor this fully, this is temp to get it out of the research times gui file
]]
local Feature = require("modules/exp_scenario/features")
local research_data = require("modules/exp_scenario/config/research_data")
local feature, config = Feature.register("research", {
pollution_ageing_by_research = false, -- pollution ages faster with each level of the bonus inventory researches
bonus_inventory = { -- extra inventory slots for each level of these researches
enabled = true,
res = Feature.set{ "mining-productivity", "mining-productivity-2", "mining-productivity-3", "mining-productivity-4" },
rate = 5,
limit = 20,
},
})
local bonus_inventory_name = "character_inventory_slots_bonus"
--- @param event EventData.on_research_finished
local function on_research_finished(event)
local research_name = event.research.name
if config.bonus_inventory.enabled and config.bonus_inventory.res[research_name] then
event.research.force[bonus_inventory_name] = math.min((event.research.level - 1) * config.bonus_inventory.rate, config.bonus_inventory.limit)
end
if config.pollution_ageing_by_research and config.bonus_inventory.res[research_name] then
game.map_settings.pollution.ageing = math.min(10, event.research.level / 5)
end
end
--- @param event EventData.on_research_started
local function on_research_started(event)
local limit = research_data.limit_res[event.research.name]
if limit and event.research.level > limit then
event.research.enabled = false
event.research.visible_when_disabled = true
local rq = event.research.force.research_queue
for i = #rq, 1, -1 do
if rq[i] == event.research.name then
table.remove(rq, i)
end
end
event.research.force.cancel_current_research()
event.research.force.research_queue = rq
end
end
local e = defines.events
return feature:guard{
events = {
[e.on_research_finished] = on_research_finished,
[e.on_research_reversed] = on_research_finished,
[e.on_research_started] = on_research_started,
[e.on_research_queued] = on_research_started,
}
}
+22 -24
View File
@@ -2,7 +2,9 @@
Adds a custom spawn area with chests and afk turrets
]]
local config = require("modules.exp_legacy.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
@@ -33,7 +35,7 @@ end
apply_offset_to_array(config.turrets.locations, config.turrets.offset, 1, 2)
apply_offset_to_array(config.afk_belts.locations, config.afk_belts.offset, 1, 2)
apply_offset_to_array(config.water.locations, config.water.offset, 1, 2)
apply_offset_to_array(config.water.locations, config.water.offset, 1, 2)
apply_offset_to_array(config.pattern.locations, config.pattern.offset, 1, 2)
apply_offset_to_array(config.entities.locations, config.entities.offset, 2, 3)
--- Get or create the force used for entities in spawn
@@ -251,30 +253,26 @@ end
local on_nth_tick = {}
if config.turrets.enabled then
--- Refill the ammo in the spawn turrets
on_nth_tick[config.turrets.refill_time] = function()
if game.tick < 10 then return end
update_turrets()
end
--- Refill the ammo in the spawn turrets
on_nth_tick[60 * 60 * 5] = function()
if game.tick < 10 or not config.turrets.enabled then return end
update_turrets()
end
if config.resource_refill_nearby.enabled then
---
on_nth_tick[config.resource_refill_nearby.refill_time] = function()
if game.tick < 10 then return end
--- Refill the resources near spawn
on_nth_tick[60 * 60 * 10] = function()
if game.tick < 10 or not config.resource_refill_nearby.enabled then return end
local force = game.forces.player
local surface = assert(game.get_surface("nauvis"))
local entities = surface.find_entities_filtered{
position = force.get_spawn_position(surface),
radius = config.resource_refill_nearby.range,
name = config.resource_refill_nearby.resources_name
}
local force = game.forces.player
local surface = assert(game.get_surface("nauvis"))
local entities = surface.find_entities_filtered{
position = force.get_spawn_position(surface),
radius = config.resource_refill_nearby.range,
name = config.resource_refill_nearby.resources_name
}
for _, ore in ipairs(entities) do
ore.amount = ore.amount + math.random(config.resource_refill_nearby.amount[1], config.resource_refill_nearby.amount[2])
end
for _, ore in ipairs(entities) do
ore.amount = ore.amount + math.random(config.resource_refill_nearby.amount[1], config.resource_refill_nearby.amount[2])
end
end
@@ -302,9 +300,9 @@ end
local e = defines.events
return {
return feature:guard{
on_nth_tick = on_nth_tick,
events = {
[e.on_player_created] = on_player_created,
}
},
}
@@ -2,7 +2,13 @@
Automatically name stations when they are placed based on closest resource and direction from spawn
]]
local config = require("modules.exp_legacy.config.station_auto_name")
local Feature = require("modules/exp_scenario/features")
local feature, config = Feature.register("station_auto_name", {
-- the name given to new stations, placeholders:
-- __icon__ __item_name__ __backer_name__ __direction__ __x__ __y__
station_name = "[L] __icon__",
})
local get_direction do
local directions = {
@@ -98,9 +104,9 @@ end
local e = defines.events
return {
return feature:guard{
events = {
[e.on_built_entity] = rename_station,
[e.on_robot_built_entity] = rename_station,
}
},
}
+56 -12
View File
@@ -4,7 +4,43 @@ Stores solar panels, accumulators and landfill in a virtual layer which produces
local ExpUtil = require("modules/exp_util")
local Storage = require("modules/exp_util/storage")
local config = require("modules.exp_legacy.config.vlayer")
local Feature = require("modules/exp_scenario/features")
local items = require("modules/exp_scenario/config/vlayer_items")
local feature, config = Feature.register("vlayer", {
unlimited_capacity = false, -- an unlimited energy capacity, accumulators are not required
unlimited_surface_area = false, -- an unlimited surface area, landfill is not required
modded_auto_downgrade = false, -- modded items are converted into their base game equivalent, the originals can not be recovered
power_on_space = false, -- energy interfaces can be built on platforms
power_on_space_research = { -- the research level needed to build them there
name = "research-productivity",
level = 10,
},
mimic_surface = "nauvis", -- the surface the day cycle is copied from, empty to use the fixed cycle below
interface_limit = { -- how many of each interface can exist
energy = 1, -- >1 allows for disconnected power networks to receive power
circuit = 20,
storage_input = 20,
storage_output = 1, -- >0 allows for item teleportation of the allowed items
},
})
local update_tick_storage = 60
local update_tick_energy = 10
--- The day cycle used when there is no mimic surface, see LuaSurface for details
local surface_settings = {
always_day = false,
solar_power_multiplier = 1,
min_brightness = 0.15,
ticks_per_day = 25200,
daytime = 0,
dusk = 0.25,
evening = 0.45,
morning = 0.55,
dawn = 0.75,
}
local floor = math.floor
local min = math.min
@@ -58,6 +94,9 @@ local mega = 1000000
--- @class ExpScenario_Vlayer
local Vlayer = {
--- The config of the feature, read only
config = config,
feature = feature,
--- @package
events = {},
--- @package
@@ -66,11 +105,11 @@ local Vlayer = {
--- The properties of every allowed item, modded items are derived from their base game equivalent
local allowed_items = {} --- @type table<string, Vlayer.ItemProperties>
for name, properties in pairs(config.allowed_items) do
for name, properties in pairs(items.allowed_items) do
allowed_items[name] = properties
end
for name, properties in pairs(config.modded_items) do
local base = config.allowed_items[properties.base_game_equivalent]
for name, properties in pairs(items.modded_items) do
local base = items.allowed_items[properties.base_game_equivalent]
local multiplier = properties.multiplier
allowed_items[name] = {
starting_value = properties.starting_value or 0,
@@ -103,7 +142,7 @@ local vlayer_data = {
power_items = {},
energy = 0,
},
surface = table.deep_copy(config.surface),
surface = table.deep_copy(surface_settings),
}
Storage.register(vlayer_data, function(tbl)
@@ -490,7 +529,7 @@ local function handle_input_interfaces()
end
if removed > 0 then
local modded = config.modded_items[name]
local modded = items.modded_items[name]
if modded and config.modded_auto_downgrade then
Vlayer.insert_item(modded.base_game_equivalent, stored * modded.multiplier)
elseif vlayer_data.storage.power_items[name] then
@@ -595,7 +634,7 @@ end
local function handle_energy_interfaces()
local storage = vlayer_data.storage
local properties = vlayer_data.properties
local production = properties.production * mega * (config.update_tick_energy / 60)
local production = properties.production * mega * (update_tick_energy / 60)
storage.energy = storage.energy + floor(production * get_production_multiplier())
local interfaces = vlayer_data.interfaces.energy
@@ -634,7 +673,7 @@ end
--- Take the day cycle from the mimic surface when it exists, otherwise from the config
local function update_surface()
if config.mimic_surface then
if config.mimic_surface ~= "" then
local surface = game.get_surface(config.mimic_surface)
if surface then
vlayer_data.surface = surface
@@ -644,7 +683,7 @@ local function update_surface()
-- The config copy never has an index, so this avoids replacing it every time a surface changes
if not vlayer_data.surface.index then
vlayer_data.surface = table.deep_copy(config.surface)
vlayer_data.surface = table.deep_copy(surface_settings)
end
end
@@ -667,7 +706,12 @@ Vlayer.on_init = update_surface -- The default surface exists before on_surface_
Vlayer.events[e.on_surface_created] = update_surface
Vlayer.events[e.on_surface_renamed] = update_surface
Vlayer.events[e.on_surface_imported] = update_surface
Vlayer.on_nth_tick[config.update_tick_storage] = update_storage
Vlayer.on_nth_tick[config.update_tick_energy] = update_energy
Vlayer.events[Feature.events.on_config_updated] = function(event)
if event.feature_name == feature.name and event.path == "mimic_surface" then
update_surface()
end
end
Vlayer.on_nth_tick[update_tick_storage] = update_storage
Vlayer.on_nth_tick[update_tick_energy] = update_energy
return Vlayer
return feature:guard(Vlayer)
+282
View File
@@ -0,0 +1,282 @@
--[[-- ExpScenario - Features
Features which the controller can enable, disable, and configure while the game is running
The config table is changed in place when the controller sends values, so read
them where they are used rather than caching them when the file loads. Values
read while the file loads, such as on_nth_tick periods, are not listed in features.ts.
--- Registering a feature, small configs live at the top of the file which uses them:
local Feature = require("modules/exp_scenario/features")
local feature, config = Feature.register("my_feature", {
show_message = true, -- booleans, numbers, and strings
role_id = Feature.optional("number"), -- no default
item_names = Feature.set{ "iron-plate" }, -- indexed by name
surface_names = Feature.list{ "nauvis" },
section = { count = 1 }, -- addressed as "section.count"
})
--- Handlers do nothing while the feature is disabled:
return feature:guard{
events = { [defines.events.on_player_joined_game] = on_player_joined_game },
}
--- Toolbar buttons and commands:
Gui.toolbar.create_button{ visible = function(player) return feature:is_enabled() end }
Commands.new("my_command", { "..." }):add_flags{ feature = feature }
--- Changes are raised per value:
[Feature.events.on_config_updated] = function(event) end, -- { feature_name, path, old_value, new_value }
]]
local Storage = require("modules/exp_util/storage")
--- @class ExpScenario.FeatureField
--- @field path string
--- @field parent table
--- @field key string
--- @field default any
--- @field type string "boolean", "number", "string", "list", "set", or a type which can not be set
--- @class ExpScenario.Features
local Feature = {
_registered = {}, --- @type table<string, ExpScenario.Feature>
events = {
--- @type EventData.ExpScenario.on_config_updated
on_config_updated = script.generate_event_name(),
},
}
--- @class ExpScenario.Feature
--- @field name string
--- @field config table
--- @field fields table<string, ExpScenario.FeatureField> By path
--- @field values table<string, any> Last received from the controller, restored from storage on load
Feature._prototype = {}
--- @class EventData.ExpScenario.on_config_updated : EventData
--- @field feature_name string
--- @field path string "enabled" when the feature is enabled or disabled
--- @field old_value any
--- @field new_value any
local Feature_mt = {
__index = Feature._prototype,
}
--- Only the values are stored, the rest is rebuilt when the file loads
local stored_values = {} --- @type table<string, table<string, any>>
Storage.register(stored_values, function(tbl)
stored_values = tbl
for name, feature in pairs(Feature._registered) do
feature.values = tbl[name] or {}
for path, field in pairs(feature.fields) do
feature:_set_field(field, feature.values[path])
end
end
end)
--- @param value any
--- @return boolean
local function is_string_list(value)
if type(value) ~= "table" then return false end
if next(value) ~= nil and value[1] == nil then return false end
for _, item in pairs(value) do
if type(item) ~= "string" then return false end
end
return true
end
--- Lists and sets are compared by their contents
--- @param a any
--- @param b any
--- @return boolean
local function values_equal(a, b)
if type(a) ~= "table" or type(b) ~= "table" then return a == b end
if table_size(a) ~= table_size(b) then return false end
for key, value in pairs(a) do
if b[key] ~= value then return false end
end
return true
end
--- Marks a default made by Feature.list, Feature.set, or Feature.optional
local typed_mt = {}
--- @param list string[]
--- @return table<string, true>
local function list_to_set(list)
local set = {}
for _, item in ipairs(list) do
set[item] = true
end
return set
end
--- Nested tables with string keys are sections
--- @param fields table<string, ExpScenario.FeatureField>
--- @param tbl table
--- @param prefix string
local function collect_fields(fields, tbl, prefix)
for key, value in pairs(tbl) do
if type(key) == "string" then
local path = prefix .. key
local field = { path = path, parent = tbl, key = key, default = value, type = type(value) }
if type(value) == "table" then
if getmetatable(value) == typed_mt then
field.type = value.type
field.default = value.default
tbl[key] = value.default
elseif type(next(value)) == "string" then
collect_fields(fields, value, path .. ".")
field = nil
end
end
if field then
fields[path] = field
end
end
end
end
--- A value with no default, the argument is the type it accepts
--- @param value_type "boolean" | "number" | "string"
--- @return any
function Feature.optional(value_type)
return setmetatable({ type = value_type }, typed_mt)
end
--- A list of strings
--- @param items string[]
--- @return string[]
function Feature.list(items)
return setmetatable({ type = "list", default = items }, typed_mt)
end
--- A set of strings, the controller sends a list
--- @param items string[]
--- @return table<string, true>
function Feature.set(items)
return setmetatable({ type = "set", default = list_to_set(items) }, typed_mt)
end
--- Features are enabled unless the config says otherwise
--- @param name string
--- @param config table?
--- @return ExpScenario.Feature feature
--- @return table config
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 fields = {}
collect_fields(fields, config, "")
local feature = setmetatable({
name = name,
config = config,
fields = fields,
values = {},
}, Feature_mt) --[[@as ExpScenario.Feature]]
Feature._registered[name] = feature
return feature, config
end
--- @param name string
--- @return ExpScenario.Feature
function Feature.get(name)
return Feature._registered[name] or error("Unknown feature: " .. tostring(name), 2)
end
--- @param field ExpScenario.FeatureField
--- @param value any
--- @return boolean
local function is_valid(field, value)
if field.type == "list" or field.type == "set" then
return is_string_list(value)
end
return type(value) == field.type and (field.type == "boolean" or field.type == "number" or field.type == "string")
end
--- Set a value, nil or an invalid value means the default
--- @package
--- @param field ExpScenario.FeatureField
--- @param value any
--- @return any # The value which was set
function Feature._prototype:_set_field(field, value)
if value ~= nil and not is_valid(field, value) then
log("[WARNING] Invalid value for " .. self.name .. "." .. field.path .. ", using the default")
value = nil
end
if value == nil then
value = field.default
elseif field.type == "set" then
value = list_to_set(value)
end
field.parent[field.key] = value
return value
end
--- @return boolean
function Feature._prototype:is_enabled()
return self.config.enabled
end
--- Make the event and on_nth_tick handlers of a lib do nothing while the feature is disabled
--- @generic T : table
--- @param lib T
--- @return T
function Feature._prototype: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)
if config.enabled then
return handler(event)
end
end
end
end
return lib
end
--- Replace every value, then raise on_config_updated for each one which changed
--- @param values table<string, any> By path, "enabled" included
function Feature._prototype:update_config(values)
self.values = values
stored_values[self.name] = values
local updates = {}
for path, field in pairs(self.fields) do
local old_value = field.parent[field.key]
local new_value = self:_set_field(field, values[path])
if not values_equal(old_value, new_value) then
updates[#updates + 1] = { feature_name = self.name, path = path, old_value = old_value, new_value = new_value }
end
end
for _, update in ipairs(updates) do
script.raise_event(Feature.events.on_config_updated, update)
end
end
--- @class ExpScenario.FeatureUpdate
--- @field name string
--- @field values table<string, any>
--- Called over rcon by the instance plugin, unknown features are ignored
--- @param updates ExpScenario.FeatureUpdate[]
function Feature.receive_update(updates)
for _, update in ipairs(updates) do
local feature = Feature._registered[update.name]
if feature then
feature:update_config(update.values)
end
end
end
return Feature
+6
View File
@@ -0,0 +1,6 @@
--- @diagnostic disable: global-in-non-module
-- Access using `/sc exp_scenario.features.foo()`
exp_scenario = {
features = require("modules/exp_scenario/features"),
}
+7 -4
View File
@@ -2,9 +2,12 @@
Adds a config menu for setting autofill of placed entities
]]
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 config = require("modules/exp_scenario/config/autofill")
local feature = Feature.register("autofill")
local FlyingText = require("modules/exp_util/flying_text")
local min = math.min
@@ -330,7 +333,7 @@ Gui.toolbar.create_button{
sprite = config.icon,
tooltip = { "exp-gui_autofill.tooltip-main" },
visible = function(player, element)
return Roles.player_has_permission(player, "exp_scenario.gui.autofill")
return feature:is_enabled() and Roles.player_has_permission(player, "exp_scenario.gui.autofill")
end
}
@@ -396,9 +399,9 @@ end
local e = defines.events
return {
return feature:guard{
elements = Elements,
events = {
[e.on_built_entity] = on_built_entity,
}
},
}
+26 -14
View File
@@ -9,17 +9,29 @@ local Roles = require("modules/exp_roles")
local Selection = require("modules/exp_util/selection")
local SelectArea = Selection.connect("ModuleArea")
local config = require("modules/exp_legacy/config/module")
local Feature = require("modules/exp_scenario/features")
local machine_sets = require("modules/exp_scenario/config/module_inserter_machines")
local feature, config = Feature.register("module_inserter", {
copy_paste_module = true, -- copying a machine also copies its modules
copy_paste_rotation = false, -- copying a machine also copies its rotation
})
local module_slots_per_row = 4
local module_slot_max = 8
--- The machines of the active mod sets
local machines = {}
--- @class ExpGui_ModuleInserter.elements
local Elements = {}
--- Load all the valid machines from the config file
local machine_names = {}
for mod_name, machine_set in pairs(config.machine_sets) do
for mod_name, machine_set in pairs(machine_sets) do
if script.active_mods[mod_name] then
for machine_name, v in pairs(machine_set) do
config.machines[machine_name] = v
machines[machine_name] = v
table.insert(machine_names, machine_name)
end
end
@@ -137,7 +149,7 @@ Elements.module_selector = Gui.define("module_inserter/module_selector")
Elements.module_table = Gui.define("module_inserter/module_table")
:draw(function(def, parent)
--- @cast def ExpGui_ModuleInserter.elements.module_table
local slots_per_row = config.module_slots_per_row + 1
local slots_per_row = module_slots_per_row + 1
return Gui.elements.scroll_table(parent, 280, slots_per_row)
end)
:element_data{} --[[@as any]]
@@ -163,12 +175,12 @@ function Elements.module_table.add_row(module_table)
}
-- Add the module selectors and row separators
local slots_per_row = config.module_slots_per_row + 1
for i = 1, config.module_slot_max do
local slots_per_row = module_slots_per_row + 1
for i = 1, module_slot_max do
if i % slots_per_row == 0 then
row_separators[#row_separators + 1] = module_table.add{ type = "flow", visible = false }
end
module_selectors[i] = Elements.module_selector(module_table, i <= config.module_slots_per_row)
module_selectors[i] = Elements.module_selector(module_table, i <= module_slots_per_row)
end
end
@@ -199,7 +211,7 @@ function Elements.module_table.reset_row(module_table, machine_selector)
separator.visible = false
end
for i, selector in pairs(row.module_selectors) do
selector.visible = i <= config.module_slots_per_row
selector.visible = i <= module_slots_per_row
selector.enabled = false
selector.elem_value = nil
end
@@ -214,16 +226,16 @@ function Elements.module_table.refresh_row(module_table, machine_selector, machi
local row = assert(rows[machine_selector.index])
local active_module_count = assert(prototypes.entity[machine_name].module_inventory_size)
local visible_row_count = math.ceil(active_module_count / config.module_slots_per_row)
local visible_module_count = visible_row_count * config.module_slots_per_row
local module_elem_value = { name = config.machines[machine_name].module }
local visible_row_count = math.ceil(active_module_count / module_slots_per_row)
local visible_module_count = visible_row_count * module_slots_per_row
local module_elem_value = { name = machines[machine_name].module }
for i, separator in pairs(row.row_separators) do
separator.visible = i < visible_row_count
end
for i, selector in pairs(row.module_selectors) do
if i <= active_module_count then
if config.machines[machine_name].prod then
if machines[machine_name].prod then
selector.elem_filters = elem_filter.with_prod
else
selector.elem_filters = elem_filter.no_prod
@@ -259,7 +271,7 @@ Gui.toolbar.create_button{
sprite = "item/productivity-module-3",
tooltip = { "exp-gui_module-inserter.tooltip-main" },
visible = function(player, element)
return Roles.player_has_permission(player, "exp_scenario.gui.module")
return feature:is_enabled() and Roles.player_has_permission(player, "exp_scenario.gui.module")
end
}
@@ -499,7 +511,7 @@ end
local e = defines.events
return {
return feature:guard{
elements = Elements,
events = {
[e.on_entity_settings_pasted] = on_entity_settings_pasted,
+52 -21
View File
@@ -4,7 +4,18 @@ Adds a gui that allows players to apply various bonuses
local Gui = require("modules/exp_gui")
local Roles = require("modules/exp_roles")
local config = require("modules/exp_legacy/config/bonus")
local Feature = require("modules/exp_scenario/features")
local feature, config = Feature.register("player_bonus", {
points = { -- how many points a player can spend on bonuses
base = 174, -- the points of the points role, the standard value of each bonus adds up to this
increase_percentage_per_role_level = 0.03, -- extra points as a fraction of the base for each role above the points role
role_id = Feature.optional("number"), -- the role the base points apply to, without one every player gets the base
},
})
--- Ticks between applications of the periodic bonuses
local periodic_bonus_rate = 300
local Vlayer = require("modules/exp_scenario/control/vlayer")
local format_number = require("util").format_number
@@ -19,11 +30,12 @@ local Elements = {}
--- @field is_percentage boolean
--- @field is_special boolean
--- @field value_step number
--- @field combined_bonus string[]? Bonuses applied together with this one
--- @field _cost_scale number
--- For perf calculate the division of scale against cost ahead of time
--- @type table<string, ExpGui_PlayerBonus.bonus_data>
local player_bonus = config.player_bonus
--- @type ExpGui_PlayerBonus.bonus_data[]
local player_bonus = require("modules/exp_scenario/config/player_bonus")
for _, bonus_data in pairs(player_bonus) do
bonus_data._cost_scale = bonus_data.cost / bonus_data.scale
@@ -71,8 +83,8 @@ do local _points_limit = {} --- @type table<number, number>
local roles = Roles.get_roles()
local positions = {}
for index, role in ipairs(roles) do positions[role.id] = index end
local role = assert(Roles.get_role_by_name(config.points.role_name), "Bonus points role does not exist")
local role_diff = positions[role.id] - positions[Roles.get_player_highest_role(player).id]
local role = config.points.role_id and Roles.get_role(config.points.role_id)
local role_diff = role and positions[role.id] - positions[Roles.get_player_highest_role(player).id] or 0
local points_limit = math.floor(config.points.base * (1 + config.points.increase_percentage_per_role_level * role_diff))
_points_limit[player.index] = points_limit
return points_limit
@@ -375,7 +387,7 @@ Elements.container = Gui.define("player_bonus/container")
Elements.reset_button.link_apply_button(elements.reset_button, elements.apply_button)
Elements.apply_button.link_reset_button(elements.apply_button, elements.reset_button)
for _, bonus_data in pairs(config.player_bonus) do
for _, bonus_data in pairs(player_bonus) do
--- @cast bonus_data ExpGui_PlayerBonus.bonus_data
Elements.bonus_table.add_row(bonus_table, bonus_data, elements)
end
@@ -409,7 +421,7 @@ function Elements.container.clear_player_bonus(player)
if not player.character then
return
end
for _, bonus_data in pairs(config.player_bonus) do
for _, bonus_data in pairs(player_bonus) do
if not bonus_data.is_special then
player[bonus_data.name] = 0
if bonus_data.combined_bonus then
@@ -429,7 +441,7 @@ function Elements.container.apply_player_bonus(player)
end
local player_data = Elements.container.data[player]
for _, bonus_data in pairs(config.player_bonus) do
for _, bonus_data in pairs(player_bonus) do
if not bonus_data.is_special then
local value = player_data[bonus_data.name] or 0
player[bonus_data.name] = value
@@ -462,27 +474,45 @@ Gui.toolbar.create_button{
sprite = "item/exoskeleton-equipment",
tooltip = { "exp-gui_player-bonus.tooltip-main" },
visible = function(player, element)
return Roles.player_has_permission(player, "exp_scenario.gui.bonus")
return feature:is_enabled() and Roles.player_has_permission(player, "exp_scenario.gui.bonus")
end
}
--- Recalculate and apply the bonus for a player
--- Recalculate the limit of a player, their bonus is removed when it is exceeded
--- @param player LuaPlayer
--- @return boolean # False when the bonus was removed
local function refresh_points_limit(player)
Elements.bonus_used._clear_points_limit_cache(player)
local bonus_cost = Elements.container.calculate_cost(player)
local within_limit = Elements.bonus_used.refresh_player(player, bonus_cost)
if not within_limit or not Roles.player_has_permission(player, "exp_scenario.gui.bonus") then
Elements.container.clear_player_bonus(player)
return false
end
return true
end
local function recalculate_bonus(event)
local player = Gui.get_player(event)
if event.name == Roles.events.on_player_roles_changed then
-- If the player's roles changed then we will need to recalculate their limit
Elements.bonus_used._clear_points_limit_cache(player)
local bonus_cost = Elements.container.calculate_cost(player)
local within_limit = Elements.bonus_used.refresh_player(player, bonus_cost)
if not within_limit or not Roles.player_has_permission(player, "exp_scenario.gui.bonus") then
Elements.container.clear_player_bonus(player)
return
end
if event.name == Roles.events.on_player_roles_changed and not refresh_points_limit(player) then
return
end
Elements.container.apply_player_bonus(player)
end
--- The points of every player change with the points settings
--- @param event EventData.ExpScenario.on_config_updated
local function on_config_updated(event)
if event.feature_name ~= feature.name then return end
for _, player in pairs(game.connected_players) do
if refresh_points_limit(player) then
Elements.container.apply_player_bonus(player)
end
end
end
--- Apply periodic bonus to a player
--- @param player LuaPlayer
local function apply_personal_battery_recharge(player)
@@ -501,7 +531,7 @@ local function apply_personal_battery_recharge(player)
return -- No grid or already full
end
local recharge_amount = Elements.container.get_player_bonus(player, "personal_battery_recharge") * 100000 * config.periodic_bonus_rate / 6
local recharge_amount = Elements.container.get_player_bonus(player, "personal_battery_recharge") * 100000 * periodic_bonus_rate / 6
for _, equipment in pairs(grid.equipment) do
if equipment.energy < equipment.max_energy then
@@ -524,13 +554,14 @@ end
local e = defines.events
return {
return feature:guard{
elements = Elements,
events = {
[e.on_player_respawned] = recalculate_bonus,
[Roles.events.on_player_roles_changed] = recalculate_bonus,
[Feature.events.on_config_updated] = on_config_updated,
},
on_nth_tick = {
[config.periodic_bonus_rate] = apply_periodic_bonus_online,
[periodic_bonus_rate] = apply_periodic_bonus_online,
}
}
+5 -5
View File
@@ -5,7 +5,7 @@ Adds a player list to show names and play time; also includes action buttons whi
local ExpUtil = require("modules/exp_util")
local Gui = require("modules/exp_gui")
local Roles = require("modules/exp_roles")
local config = require("modules/exp_legacy/config/gui/player_list_actions")
local actions = require("modules/exp_scenario/gui/player_list_actions")
--- @class ExpGui_PlayerList.elements
local Elements = {}
@@ -100,7 +100,7 @@ Elements.reason_confirm = Gui.define("player_list/reason_confirm")
}
:on_click(function(_, player, element)
local action_name = Elements.container.get_selected_action(player)
local button_data = action_name and config.buttons[action_name]
local button_data = action_name and actions.buttons[action_name]
if button_data and button_data.reason_callback then
local reason = assert(element.parent).entry.text --[[@as string?]]
if reason == nil or not reason:find("%S") then reason = "no reason given" end
@@ -330,7 +330,7 @@ Elements.action_bar = Gui.define("player_list/action_bar")
action_bar.visible = false
Elements.close_action_bar(action_bar)
for action_name, button_data in pairs(config.buttons) do
for action_name, button_data in pairs(actions.buttons) do
local permission_flow = action_bar.add{ type = "flow", name = action_name }
permission_flow.visible = false
for _, button in ipairs(button_data) do
@@ -356,7 +356,7 @@ function Elements.action_bar.refresh(action_bar, player, selected_player)
end
action_bar.visible = true
for action_name, buttons in pairs(config.buttons) do
for action_name, buttons in pairs(actions.buttons) do
local flow = action_bar[action_name]
if buttons.auth and not buttons.auth(player, selected_player) then
flow.visible = false
@@ -483,7 +483,7 @@ local function select_action(player, selected_action)
Elements.container.set_selected_action(player, selected_action)
Elements.player_table.refresh_player(player)
end
config.set_accessors(Elements.container.get_selected_player, select_action)
actions.set_accessors(Elements.container.get_selected_player, select_action)
--- Add the element to the left flow with a toolbar button
Gui.add_left_element(Elements.container, true)
@@ -1,9 +1,7 @@
--- Config for the different action buttons that show on the player list;
-- each button has the button define(s) given along side an auth function, and optional reason callback;
-- if a reason callback is used then Store.set(action_name_store,player.name,'BUTTON_NAME') should be called during on_click;
-- buttons can be removed from the gui by commenting them out of the config at the bottom of this file;
-- the key used for the name of the button is the permission name used by the role system;
-- @config Player-List
--[[-- Gui - Player List Actions
The action buttons shown on the player list, each with its auth function and optional reason callback.
The key of each button is the permission name used by the role system.
]]
local ExpUtil = require("modules/exp_util")
local Gui = require("modules/exp_gui")
+11
View File
@@ -4,6 +4,7 @@ Adds a few buttons for common actions
local Gui = require("modules/exp_gui")
local Commands = require("modules/exp_commands")
local Feature = require("modules/exp_scenario/features")
local Roles = require("modules/exp_roles")
local addon_artillery = require("modules/exp_scenario/commands/artillery")
@@ -126,9 +127,19 @@ local function on_role_changed(event)
Elements.container.refresh_player(player)
end
--- Actions for commands of a disabled feature are hidden
--- @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
end
return {
elements = Elements,
events = {
[Roles.events.on_player_roles_changed] = on_role_changed,
[Feature.events.on_config_updated] = on_config_updated,
}
}
+12 -15
View File
@@ -5,7 +5,12 @@ Adds a gui for tracking research milestones
local ExpUtil = require("modules/exp_util")
local Gui = require("modules/exp_gui")
local Roles = require("modules/exp_roles")
local config = require("modules/exp_legacy/config/research")
local Feature = require("modules/exp_scenario/features")
local research_data = require("modules/exp_scenario/config/research_data")
local feature, config = Feature.register("research_milestones", {
log_file = "log/research.log", -- where the milestone times are written
})
local table_to_json = helpers.table_to_json
local write_file = helpers.write_file
@@ -34,18 +39,10 @@ local research_targets = {
length = 0,
}
--- Select the mod set to be used for milestones
for _, mod_name in ipairs(config.mod_set_lookup) do
if script.active_mods[mod_name] then
config.mod_set = mod_name
break
end
end
do --- Calculate the research targets
local research_index = 1
local total_time = 0
for name, time in pairs(config.milestone[config.mod_set]) do
for name, time in pairs(research_data.milestone[research_data.mod_set]) do
if prototypes.technology[name] and (not prototypes.technology[name].hidden) then
research_targets.index_lookup[name] = research_index
total_time = total_time + time * 60
@@ -328,7 +325,7 @@ function Elements.container.append_log_line(force)
result_data[name] = force_data[research_index]
end
write_file(config.file.name, table_to_json(result_data) .. "\n", true, 0)
write_file(config.log_file, table_to_json(result_data) .. "\n", true, 0)
end
--- Add the element to the left flow with a toolbar button
@@ -339,7 +336,7 @@ Gui.toolbar.create_button{
sprite = "item/space-science-pack",
tooltip = { "exp-gui_research-milestones.tooltip-main" },
visible = function(player, element)
return Roles.player_has_permission(player, "exp_scenario.gui.research")
return feature:is_enabled() and Roles.player_has_permission(player, "exp_scenario.gui.research")
end
}
@@ -350,8 +347,8 @@ local function on_research_finished(event)
local force = event.research.force
-- Check if the log should be updated and print a message to chat
if config.inf_res[config.mod_set][research_name] then
local log_requirement = config.file.final_res[config.mod_set]
if research_data.inf_res[research_data.mod_set][research_name] then
local log_requirement = research_data.final_res[research_data.mod_set]
if research_name == log_requirement.name and research_level == log_requirement.level + 1 then
Elements.container.append_log_line(force)
end
@@ -394,7 +391,7 @@ end
local e = defines.events
return {
return feature:guard{
elements = Elements,
events = {
[e.on_research_finished] = on_research_finished,
+35 -17
View File
@@ -5,7 +5,25 @@ Adds a rocket information gui which shows general stats, milestones and build pr
local ExpUtil = require("modules/exp_util")
local Gui = require("modules/exp_gui")
local Roles = require("modules/exp_roles")
local config = require("modules/exp_legacy/config/gui/rockets")
local Feature = require("modules/exp_scenario/features")
local feature, config = Feature.register("rocket_info", {
show_stats = true, -- the stats section
show_first_rocket = true, -- when the first rocket was launched
show_last_rocket = true, -- when the last rocket was launched
show_fastest_rocket = true, -- the time taken for the fastest rocket
show_total_rockets = true, -- the total number of rockets launched
show_game_avg = true, -- the average across the entire map time
show_milestones = true, -- the milestones section
show_progress = true, -- the build progress section
allow_zoom_to_map = true, -- the zoom to map button
})
--- Each number is one statistic, 5 means the average time taken for the last 5 rockets
local rolling_avg = { 5, 10, 25 }
--- Each number is one statistic, 5 means the time that the 5th rocket was launched
local milestones = { 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000 }
--- @class ExpGui_RocketInfo.elements
local Elements = {}
@@ -26,7 +44,7 @@ local font_color = {
-- The largest rolling average is used to know when an old launch time can be discarded
local largest_rolling_avg = 0
for _, avg_over in pairs(config.stats.rolling_avg) do
for _, avg_over in pairs(rolling_avg) do
if avg_over > largest_rolling_avg then
largest_rolling_avg = avg_over
end
@@ -110,7 +128,7 @@ Elements.position_label = Gui.define("rocket_info/position_label")
)
:on_click(function(def, player, element)
--- @cast def ExpGui_RocketInfo.elements.position_label
if not config.progress.allow_zoom_to_map then return end
if not config.allow_zoom_to_map then return end
local entity = def.data[element]
if not entity or not entity.valid then return end
player.set_controller{ type = defines.controllers.remote, position = entity.position, surface = entity.surface }
@@ -142,34 +160,34 @@ function Elements.stats_table.calculate_row_data(force)
local force_rockets = force.rockets_launched
local rows = {}
if config.stats.show_first_rocket then
if config.show_first_rocket then
local value = stats.first_launch or 0
rows[#rows + 1] = { key = "first-launch", name = "first-launch", value = time_formats.caption_hours(value), tooltip = time_formats.tooltip_hours(value) }
end
if config.stats.show_last_rocket then
if config.show_last_rocket then
local value = stats.last_launch or 0
rows[#rows + 1] = { key = "last-launch", name = "last-launch", value = time_formats.caption_hours(value), tooltip = time_formats.tooltip_hours(value) }
end
if config.stats.show_fastest_rocket then
if config.show_fastest_rocket then
local value = stats.fastest_launch or 0
rows[#rows + 1] = { key = "fastest-launch", name = "fastest-launch", value = time_formats.caption_hours(value), tooltip = time_formats.tooltip_hours(value) }
end
if config.stats.show_total_rockets then
if config.show_total_rockets then
local total_rockets = get_game_rocket_count()
if total_rockets == 0 then total_rockets = 1 end
local percentage = math.floor(force_rockets / total_rockets * 1000) / 10
rows[#rows + 1] = { key = "total-rockets", name = "total-rockets", value = tostring(force_rockets), tooltip = { "exp-gui_rocket-info.value-tooltip-total-rockets", percentage } }
end
if config.stats.show_game_avg then
if config.show_game_avg then
local avg = force_rockets > 0 and math.floor(game.tick / force_rockets) or 0
rows[#rows + 1] = { key = "avg-launch", name = "avg-launch", value = time_formats.caption(avg), tooltip = time_formats.tooltip(avg) }
end
for _, avg_over in pairs(config.stats.rolling_avg) do
for _, avg_over in pairs(rolling_avg) do
local avg = get_rolling_average(force, avg_over)
rows[#rows + 1] = { key = "avg-launch-n-" .. avg_over, name = "avg-launch-n", subname = avg_over, value = time_formats.caption(avg), tooltip = time_formats.tooltip(avg) }
end
@@ -258,7 +276,7 @@ function Elements.milestones_table.calculate_row_data(force)
local force_rockets = force.rockets_launched
local rows = {}
for _, milestone in ipairs(config.milestones) do
for _, milestone in ipairs(milestones) do
-- The milestones config mixes the show_milestones flag with the milestone numbers
if type(milestone) == "number" then
if milestone <= force_rockets then
@@ -421,7 +439,7 @@ end
--- @param row_data ExpGui_RocketInfo.elements.progress_table.row_data
function Elements.progress_table.add_row(progress_table, row_data)
local rows = Elements.progress_table.data[progress_table].rows
local zoom_tooltip = config.progress.allow_zoom_to_map and { "exp-gui_rocket-info.progress-label-tooltip" } or nil
local zoom_tooltip = config.allow_zoom_to_map and { "exp-gui_rocket-info.progress-label-tooltip" } or nil
local x = Elements.position_label(progress_table, { caption = row_data.x, tooltip = zoom_tooltip, entity = row_data.entity })
local y = Elements.position_label(progress_table, { caption = row_data.y, tooltip = zoom_tooltip, entity = row_data.entity })
@@ -544,17 +562,17 @@ Elements.container = Gui.define("rocket_info/container")
local force = player.force --[[@as LuaForce]]
local force_data = def._get_force_data(force)
if config.stats.show_stats then
if config.show_stats then
local row_data = Elements.stats_table.calculate_row_data(force)
Elements.stats_table.refresh(def.add_section(container, "stats", Elements.stats_table), row_data)
end
if config.milestones.show_milestones then
if config.show_milestones then
local row_data = Elements.milestones_table.calculate_row_data(force)
Elements.milestones_table.refresh(def.add_section(container, "milestones", Elements.milestones_table), row_data)
end
if config.progress.show_progress then
if config.show_progress then
local row_data = Elements.progress_table.calculate_row_data_all(force_data.silos)
Elements.progress_table.refresh(def.add_section(container, "progress", Elements.progress_table), row_data)
end
@@ -651,7 +669,7 @@ Gui.toolbar.create_button{
sprite = "item/rocket-silo",
tooltip = { "exp-gui_rocket-info.tooltip-main" },
visible = function(player, element)
return Roles.player_has_permission(player, "exp_scenario.gui.rocket_info")
return feature:is_enabled() and Roles.player_has_permission(player, "exp_scenario.gui.rocket_info")
end
}
@@ -678,7 +696,7 @@ local function on_cargo_pod_finished_ascending(event)
-- Discard the launch time that is no longer needed by any rolling average unless it is a milestone
local remove_rocket = rockets_launched - largest_rolling_avg
if remove_rocket > 0 and not table.array_contains(config.milestones, remove_rocket) then
if remove_rocket > 0 and not table.array_contains(milestones, remove_rocket) then
times[remove_rocket] = nil
end
@@ -721,7 +739,7 @@ end
local e = defines.events
return {
return feature:guard{
elements = Elements,
events = {
[e.on_cargo_pod_finished_ascending] = on_cargo_pod_finished_ascending,
+30 -9
View File
@@ -6,7 +6,28 @@ local ExpUtil = require("modules/exp_util")
local Gui = require("modules/exp_gui")
local Colors = require("modules/exp_util/include/color")
local Roles = require("modules/exp_roles")
local config = require("modules/exp_legacy/config/gui/science")
local Feature = require("modules/exp_scenario/features")
local feature, config = Feature.register("science_production", {
show_eta = true, -- the eta for research completion
color_flux = 0.1, -- the fluctuation allowed in production before the icon changes color
})
--- The science packs shown, those not in the game are removed below
local science_packs = {
"automation-science-pack",
"logistic-science-pack",
"military-science-pack",
"chemical-science-pack",
"production-science-pack",
"utility-science-pack",
"space-science-pack",
"metallurgic-science-pack",
"agricultural-science-pack",
"electromagnetic-science-pack",
"cryogenic-science-pack",
"promethium-science-pack",
}
local _format_number = require("util").format_number
local clock_time_format = ExpUtil.format_time_factory_locale{ format = "clock", hours = true, minutes = true, seconds = true }
@@ -16,9 +37,9 @@ local clock_time_format_nil = { "exp-gui_science-production.caption-eta-time", c
local long_time_format_nil = long_time_format(nil)
--- Remove invalid science packs, this can result from a certain mod not being loaded
for i = #config, 1, -1 do
if not prototypes.item[config[i]] then
table.remove(config, i)
for i = #science_packs, 1, -1 do
if not prototypes.item[science_packs[i]] then
table.remove(science_packs, i)
end
end
@@ -337,7 +358,7 @@ do local _row_data = {} --- @type table<string, table<number, ExpGui_ScienceProd
if next(force.connected_players) then
local row_data = _row_data[force.name] or {}
_row_data[force.name] = row_data
for i, science_pack in ipairs(config) do
for i, science_pack in ipairs(science_packs) do
--- @cast science_pack any
row_data[i] = Elements.science_table.calculate_row_data(force, science_pack, row_data[i])
end
@@ -448,7 +469,7 @@ Elements.container = Gui.define("science_production/container")
local force = Gui.get_player(parent).force --[[@as LuaForce]]
local science_table = Elements.science_table(container)
for _, science_pack in ipairs(config) do
for _, science_pack in ipairs(science_packs) do
--- @cast science_pack any
local row_data = Elements.science_table.calculate_row_data(force, science_pack)
Elements.science_table.add_row(science_table, row_data)
@@ -522,7 +543,7 @@ do local _production_data = {} --- @type table<string, { [string]: ExpGui_Scienc
end
-- Calculate the production data for each science pack
for _, science_pack in ipairs(config) do
for _, science_pack in ipairs(science_packs) do
--- @cast science_pack any
local made, used = 0, 0
for _, stats in pairs(production_stats) do
@@ -571,7 +592,7 @@ Gui.toolbar.create_button{
sprite = "entity/lab",
tooltip = { "exp-gui_science-production.tooltip-main" },
visible = function(player, element)
return Roles.player_has_permission(player, "exp_scenario.gui.science_info")
return feature:is_enabled() and Roles.player_has_permission(player, "exp_scenario.gui.science_info")
end
}
@@ -583,7 +604,7 @@ local function update_gui()
Elements.no_production_label.refresh_online()
end
return {
return feature:guard{
elements = Elements,
on_nth_tick = {
[60] = update_gui,
File diff suppressed because it is too large. Load diff
+4 -4
View File
@@ -5,8 +5,8 @@ Shows what the virtual layer holds and lets players build and remove its interfa
local Gui = require("modules/exp_gui")
local Roles = require("modules/exp_roles")
local Colors = require("modules/exp_util/include/color")
local config = require("modules.exp_legacy.config.vlayer")
local Vlayer = require("modules/exp_scenario/control/vlayer")
local feature, config = Vlayer.feature, Vlayer.config
local format_number = require("util").format_number
local Selection = require("modules/exp_util/selection")
@@ -444,7 +444,7 @@ Gui.toolbar.create_button{
sprite = "entity/solar-panel",
tooltip = { "exp-gui_vlayer.tooltip-main" },
visible = function(player, element)
return Roles.player_has_permission(player, "exp_scenario.gui.vlayer")
return feature:is_enabled() and Roles.player_has_permission(player, "exp_scenario.gui.vlayer")
end
}
@@ -520,12 +520,12 @@ local function refresh_player_permissions(event)
Elements.controls.refresh_permissions(Gui.get_player(event))
end
return {
return feature:guard{
elements = Elements,
events = {
[Roles.events.on_player_roles_changed] = refresh_player_permissions,
},
on_nth_tick = {
[config.update_tick_gui] = Elements.stats_table.refresh_online,
[60] = Elements.stats_table.refresh_online,
}
}
+63 -30
View File
@@ -5,7 +5,7 @@ Adds a list of warp points which players can travel between, add, and edit
local Gui = require("modules/exp_gui")
local Roles = require("modules/exp_roles")
local Colors = require("modules/exp_util/include/color")
local config = require("modules.exp_legacy.config.gui.warps")
local Feature = require("modules/exp_scenario/features")
local ExpUtil = require("modules/exp_util")
local format_time = ExpUtil.format_time_factory_locale{ format = "short", hours = true, minutes = true }
@@ -31,18 +31,61 @@ local status_icons = {
different = "[img=utility/warning_white]",
}
local update_interval = floor(60 / config.update_smoothing)
local cooldown_ticks = config.cooldown_duration * 60
local proximity_radius_sq = config.standard_proximity_radius ^ 2
local spawn_radius_sq = config.spawn_proximity_radius ^ 2
local minimum_distance_sq = config.minimum_distance ^ 2
local feature, config = Feature.register("warp_list", {
minimum_distance = 100, -- the minimum distance that is allowed between warps on the same force
cooldown_duration = 60, -- the duration of the warp cooldown in seconds
standard_proximity_radius = 4, -- the minimum distance a player is allowed to be to a warp in order to use it
spawn_proximity_radius = 20, -- the minimum distance a player is allowed to be from their spawn point to use warps
user_can_edit_own_warps = false, -- the player who made a warp can edit it without the edit permission
})
--- Ticks between updates of the cooldown timers
local update_interval = 6
--- The default icon of new warps
local default_icon = { type = "item", name = "discharge-defense-equipment" }
--- The entities which are created for warp areas { name, x, y }
local area_entities = {
{ "small-lamp", -4, -2 }, { "small-lamp", -2, -4 }, { "medium-electric-pole", -3, -3 }, -- Top left corner
{ "small-lamp", 3, -2 }, { "small-lamp", 1, -4 }, { "medium-electric-pole", 2, -3 }, -- Top right corner
{ "small-lamp", 3, 1 }, { "small-lamp", 1, 3 }, { "medium-electric-pole", 2, 2 }, -- Bottom right corner
{ "small-lamp", -4, 1 }, { "small-lamp", -2, 3 }, { "medium-electric-pole", -3, 2 }, -- Bottom left corner
}
--- The tiles which are created for warp areas { name, x, y }
local area_tiles = {
{ "black-refined-concrete", -4, -2 }, { "black-refined-concrete", -4, -1 }, { "black-refined-concrete", -4, 0 }, { "black-refined-concrete", -4, 1 },
{ "black-refined-concrete", -3, -3 }, { "purple-refined-concrete", -3, -2 }, { "purple-refined-concrete", -3, -1 }, { "purple-refined-concrete", -3, 0 },
{ "purple-refined-concrete", -3, 1 }, { "black-refined-concrete", -3, 2 }, { "black-refined-concrete", -2, -4 }, { "purple-refined-concrete", -2, -3 },
{ "purple-refined-concrete", -2, -2 }, { "purple-refined-concrete", -2, -1 }, { "purple-refined-concrete", -2, 0 }, { "purple-refined-concrete", -2, 1 },
{ "purple-refined-concrete", -2, 2 }, { "black-refined-concrete", -2, 3 }, { "black-refined-concrete", -1, -4 }, { "purple-refined-concrete", -1, -3 },
{ "purple-refined-concrete", -1, -2 }, { "purple-refined-concrete", -1, -1 }, { "purple-refined-concrete", -1, 0 }, { "purple-refined-concrete", -1, 1 },
{ "purple-refined-concrete", -1, 2 }, { "black-refined-concrete", -1, 3 }, { "black-refined-concrete", 0, -4 }, { "purple-refined-concrete", 0, -3 },
{ "purple-refined-concrete", 0, -2 }, { "purple-refined-concrete", 0, -1 }, { "purple-refined-concrete", 0, 0 }, { "purple-refined-concrete", 0, 1 },
{ "purple-refined-concrete", 0, 2 }, { "black-refined-concrete", 0, 3 }, { "black-refined-concrete", 1, -4 }, { "purple-refined-concrete", 1, -3 },
{ "purple-refined-concrete", 1, -2 }, { "purple-refined-concrete", 1, -1 }, { "purple-refined-concrete", 1, 0 }, { "purple-refined-concrete", 1, 1 },
{ "purple-refined-concrete", 1, 2 }, { "black-refined-concrete", 1, 3 }, { "black-refined-concrete", 2, -3 }, { "purple-refined-concrete", 2, -2 },
{ "purple-refined-concrete", 2, -1 }, { "purple-refined-concrete", 2, 0 }, { "purple-refined-concrete", 2, 1 }, { "black-refined-concrete", 2, 2 },
{ "black-refined-concrete", 3, -2 }, { "black-refined-concrete", 3, -1 }, { "black-refined-concrete", 3, 0 }, { "black-refined-concrete", 3, 1 },
}
--- Names of the entities which make up a warp area, used to find them when the area is removed
local area_entity_names = {} --- @type string[]
for _, entity in ipairs(config.entities) do
for _, entity in ipairs(area_entities) do
area_entity_names[#area_entity_names + 1] = entity[1]
end
--- The role permission each action needs
local permissions = {
bypass_warp_cooldown = "exp_scenario.gui.warp_list.bypass_cooldown",
bypass_warp_proximity = "exp_scenario.gui.warp_list.bypass_proximity",
allow_add_warp = "exp_scenario.gui.warp_list.add",
allow_edit_warp = "exp_scenario.gui.warp_list.edit",
}
--- @class ExpGui_WarpList.Warp
--- @field id number
--- @field force LuaForce
@@ -63,21 +106,11 @@ end
--- @field on_cooldown boolean
--- @field bypass_proximity boolean
--- Check a permission from the config, which says who an action applies to
--- @param player LuaPlayer
--- @param action "bypass_warp_cooldown" | "bypass_warp_proximity" | "allow_add_warp" | "allow_edit_warp"
--- @return boolean
local function has_config_permission(player, action)
local setting = config[action]
if setting == "all" then
return true
elseif setting == "admin" then
return player.admin
elseif setting == "exp_roles" then
return Roles.player_has_permission(player, config["exp_roles_" .. action])
end
return false
return Roles.player_has_permission(player, permissions[action])
end
--- Check if a player can edit a warp, the spawn warp can never be edited
@@ -694,7 +727,7 @@ Elements.cooldown_bar = Gui.define("warp_list/cooldown_bar")
--- @param cooldown number
function Elements.cooldown_bar.refresh(cooldown_bar, cooldown)
if cooldown > 0 then
cooldown_bar.value = 1 - cooldown / cooldown_ticks
cooldown_bar.value = 1 - cooldown / (config.cooldown_duration * 60)
cooldown_bar.tooltip = { "exp-gui_warp-list.tooltip-cooldown", ceil(cooldown / 60) }
else
cooldown_bar.value = 1
@@ -877,12 +910,12 @@ function Elements.container._make_area(warp)
warp.old_tile = surface.get_tile(position.x, position.y).name
local tiles = {}
for index, tile in ipairs(config.tiles) do
for index, tile in ipairs(area_tiles) do
tiles[index] = { name = tile[1], position = { x = tile[2] + position.x, y = tile[3] + position.y } }
end
surface.set_tiles(tiles)
for _, entity_config in ipairs(config.entities) do
for _, entity_config in ipairs(area_entities) do
local entity = assert(surface.create_entity{
name = entity_config[1],
position = { x = entity_config[2] + position.x, y = entity_config[3] + position.y },
@@ -908,7 +941,7 @@ function Elements.container._remove_area(warp)
local surface = warp.surface
local position = warp.position
local tiles = {}
for index, tile in ipairs(config.tiles) do
for index, tile in ipairs(area_tiles) do
tiles[index] = { name = old_tile, position = { x = tile[2] + position.x, y = tile[3] + position.y } }
end
surface.set_tiles(tiles)
@@ -947,7 +980,7 @@ function Elements.container.add_warp(force, surface, position, player, name)
id = id,
force = force,
name = name or "New warp",
icon = { type = config.default_icon.type, name = config.default_icon.name },
icon = { type = default_icon.type, name = default_icon.name },
surface = surface,
position = { x = floor(position.x), y = floor(position.y) },
last_user = player,
@@ -1054,8 +1087,8 @@ function Elements.container.teleport_player(player, warp)
local player_data = Elements.container._get_player_data(player)
if not has_config_permission(player, "bypass_warp_cooldown") then
player_data.cooldown = cooldown_ticks
Elements.cooldown_bar.refresh_player(player, cooldown_ticks)
player_data.cooldown = config.cooldown_duration * 60
Elements.cooldown_bar.refresh_player(player, player_data.cooldown)
end
player_data.in_range = warp
Elements.warp_table.refresh_status_player(player)
@@ -1109,7 +1142,7 @@ function Elements.container._update_proximity(player, player_data)
end
local in_range = nil --- @type ExpGui_WarpList.Warp?
if closest and closest_distance < (closest == force_data.spawn and spawn_radius_sq or proximity_radius_sq) then
if closest and closest_distance < (closest == force_data.spawn and config.spawn_proximity_radius or config.standard_proximity_radius) ^ 2 then
in_range = closest
end
if in_range ~= player_data.in_range then
@@ -1121,7 +1154,7 @@ function Elements.container._update_proximity(player, player_data)
end
local blocking = nil --- @type ExpGui_WarpList.Warp?
if closest and closest_distance <= minimum_distance_sq then
if closest and closest_distance <= config.minimum_distance ^ 2 then
blocking = closest
end
if blocking ~= player_data.blocking then
@@ -1135,10 +1168,10 @@ Gui.add_left_element(Elements.container, false)
Gui.toolbar.create_button{
name = "toggle_warp_list",
left_element = Elements.container,
sprite = config.default_icon.type .. "/" .. config.default_icon.name,
sprite = default_icon.type .. "/" .. default_icon.name,
tooltip = { "exp-gui_warp-list.tooltip-main" },
visible = function(player, element)
return Roles.player_has_permission(player, "exp_scenario.gui.warp_list")
return feature:is_enabled() and Roles.player_has_permission(player, "exp_scenario.gui.warp_list")
end
}:on_click(function(def, player)
-- A list the player opened themselves stays open when they leave a warp
@@ -1202,7 +1235,7 @@ end
local e = defines.events
return {
return feature:guard{
elements = Elements,
events = {
[e.on_player_created] = on_player_created,
+3
View File
@@ -1,6 +1,9 @@
[exp-commands-authorities_role]
deny=Unauthorized, Access is denied due to missing permissions.
[exp-commands-authorities_feature]
deny=This command is disabled on this server.
[exp-commands-parse_role]
lower-role=Role is higher than your highest.
lower-role-player=Player has a higher role.
+1
View File
@@ -3,6 +3,7 @@
"load": [
],
"require": [
"globals.lua",
"control.lua"
],
"dependencies": {
+3
View File
@@ -17,15 +17,18 @@
},
"peerDependencies": {
"@clusterio/controller": "workspace:^",
"@clusterio/host": "workspace:^",
"@clusterio/lib": "workspace:^"
},
"devDependencies": {
"@clusterio/controller": "workspace:^",
"@clusterio/host": "workspace:^",
"@clusterio/lib": "workspace:^",
"@clusterio/web_ui": "workspace:^",
"@types/node": "catalog:",
"@types/react": "catalog:",
"antd": "catalog:",
"fengari": "^0.1.5",
"react": "catalog:",
"react-dom": "catalog:",
"tap": "^21.1.0",
+62 -7
View File
@@ -4,6 +4,7 @@ import { Controller } from "@clusterio/controller";
import { ControllerPlugin } from "../dist/node/controller.js";
import * as messages from "../dist/node/messages.js";
import { seedRoles, seedGroups } from "../dist/node/seed.js";
import { features } from "../dist/node/features.js";
import * as roles from "@expcluster/roles";
import * as groups from "@expcluster/permission-groups";
import { ControllerPlugin as RolesPlugin } from "@expcluster/roles/dist/node/controller.js";
@@ -16,7 +17,7 @@ import { plugin } from "../dist/node/index.js";
lib.registerPluginPermissions([plugin]);
// The controller validates message classes against the link registry
for (const Message of [messages.SeedRequest, ...roles.plugin.messages, ...groups.plugin.messages]) {
for (const Message of [...plugin.messages, ...roles.plugin.messages, ...groups.plugin.messages]) {
lib.Link.register(Message);
}
@@ -29,9 +30,9 @@ t.after(() => {
});
/** Build the plugin around a real controller, which is side effect free while not started. */
async function startPlugin(t2, { withPlugins = true } = {}) {
async function startPlugin(t2, { withPlugins = true, databaseDirectory = t2.testdir() } = {}) {
const controllerConfig = new lib.ControllerConfig("controller", {
"controller.database_directory": t2.testdir(),
"controller.database_directory": databaseDirectory,
"controller.default_role_id": lib.Role.DefaultPlayerRoleId,
});
const controller = new Controller(logger, [], controllerConfig);
@@ -40,13 +41,11 @@ async function startPlugin(t2, { withPlugins = true } = {}) {
if (withPlugins) {
for (const [name, Plugin] of [["exp_roles", RolesPlugin], ["exp_groups", GroupsPlugin]]) {
const plugin = new Plugin({ plugin: { name }, controller, logger });
await plugin.init();
await Plugin.fromContext({ plugin: { name }, controller, logger });
}
}
const plugin = new ControllerPlugin({ plugin: { name: "exp_scenario" }, controller, logger });
await plugin.init();
const plugin = await ControllerPlugin.fromContext({ plugin: { name: "exp_scenario" }, controller, logger });
return { plugin, controller };
}
@@ -135,5 +134,61 @@ t.test("class ControllerPlugin", t2 => {
t3.notOk([...first.values()].filter(priority => priority === 2).length > 1, "seed priorities skip taken ones");
});
t2.test(".fromContext() adds every feature with its defaults", async t3 => {
const { plugin } = await startPlugin(t3, { withPlugins: false });
const records = await plugin.handleFeatureListRequest();
t3.strictSame(records.map(record => record.id), features.map(feature => feature.name), "one record per feature");
t3.ok(records.every(record => record.enabled && !Object.keys(record.values).length), "enabled with no overrides");
});
t2.test(".reconcileFeatures() drops stored values and features which no longer exist", async t3 => {
const databaseDirectory = t3.testdir();
const { plugin } = await startPlugin(t3, { withPlugins: false, databaseDirectory });
plugin.features.set(new messages.FeatureRecord("afk_kick", false, { afk_minutes: 5, trust_time: 600 }));
plugin.features.set(new messages.FeatureRecord("gone", true, {}));
await plugin.onShutdown();
const { plugin: restarted } = await startPlugin(t3, { withPlugins: false, databaseDirectory });
const record = restarted.features.get("afk_kick");
t3.strictSame([record.enabled, record.values], [false, { afk_minutes: 5 }], "the stale value is dropped, the rest kept");
t3.notOk(restarted.features.has("gone"), "the removed feature is deleted");
t3.ok(features.every(feature => restarted.features.has(feature.name)), "every declared feature still has a record");
});
t2.test(".handleFeatureUpdateRequest() stores the values which differ from the default", async t3 => {
const { plugin } = await startPlugin(t3, { withPlugins: false });
const broadcasts = [];
plugin.controller.subscriptions.broadcast = event => broadcasts.push(event);
const record = await plugin.handleFeatureUpdateRequest(new messages.FeatureUpdateRequest(
"death_markers", false, { show_map_markers: false, collect_corpses: true },
));
t3.strictSame(record.values, { show_map_markers: false }, "the default value is dropped");
t3.equal(plugin.features.get("death_markers").enabled, false, "the feature is disabled");
t3.strictSame(broadcasts.map(event => event.updates.map(update => update.id)), [["death_markers"]], "the update is broadcast");
const subscription = await plugin.handleFeatureSubscription({ lastRequestTimeMs: record.updatedAtMs - 1 });
t3.strictSame(subscription.updates.map(update => update.id), ["death_markers"], "subscribers catch up on the change");
});
t2.test(".handleFeatureUpdateRequest() rejects invalid values", async t3 => {
const { plugin } = await startPlugin(t3, { withPlugins: false });
await t3.rejects(
plugin.handleFeatureUpdateRequest(new messages.FeatureUpdateRequest("death_markers", true, { unknown: 1 })),
{ message: "Feature death_markers has no setting unknown" },
);
});
t2.test(".fromContext() keeps stored features across restarts", async t3 => {
const databaseDirectory = t3.testdir();
const { plugin } = await startPlugin(t3, { withPlugins: false, databaseDirectory });
await plugin.handleFeatureUpdateRequest(new messages.FeatureUpdateRequest("afk_kick", false, { afk_minutes: 5 }));
await plugin.onShutdown();
const { plugin: restarted } = await startPlugin(t3, { withPlugins: false, databaseDirectory });
const record = restarted.features.get("afk_kick");
t3.strictSame([record.enabled, record.values], [false, { afk_minutes: 5 }], "the stored record is loaded");
});
t2.end();
});
+117
View File
@@ -0,0 +1,117 @@
import fs from "node:fs";
import path from "node:path";
import t from "tap";
import { features, pruneFeatureValues, validateFeatureValues } from "../dist/node/features.js";
/** Every lua file in the module, by path relative to it. */
function luaFiles(dir, prefix = "") {
return fs.readdirSync(dir, { withFileTypes: true }).flatMap(entry => {
const relative = path.posix.join(prefix, entry.name);
if (entry.isDirectory()) {
return luaFiles(path.join(dir, entry.name), relative);
}
return entry.name.endsWith(".lua") ? [[relative, fs.readFileSync(path.join(dir, entry.name), "utf8")]] : [];
});
}
const moduleFiles = luaFiles(path.join(import.meta.dirname, "..", "module"));
/**
* 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, Feature.list, Feature.set, and Feature.optional.
*/
function luaDefault(source, path) {
const keys = path.split(".");
let start = 0;
for (const section of keys.slice(0, -1)) {
const match = new RegExp(`^\\s*${section} = \\{`, "m").exec(source.slice(start));
if (!match) {
return undefined;
}
start += match.index + match[0].length;
}
const key = keys.at(-1);
const match = new RegExp(`^\\s*${key} = (true|false|-?[\\d.]+|"[^"]*"|Feature\\.optional\\("\\w+"\\)|(?:Feature\\.(?:list|set))?\\{[^{}]*\\})`, "m")
.exec(source.slice(start));
if (!match) {
return undefined;
}
const literal = match[1];
if (literal.startsWith("Feature.optional")) {
return null;
}
if (literal.includes("{")) {
return [...literal.matchAll(/"([^"]*)"/g)].map(item => item[1]);
}
return JSON.parse(literal);
}
t.test("features", t2 => {
for (const feature of features) {
t2.test(feature.name, t3 => {
const declarations = moduleFiles.filter(([, source]) => (
source.includes(`Feature.register("${feature.name}"`)
));
t3.equal(declarations.length, 1, "one lua file declares the feature");
const source = declarations[0]?.[1] ?? "";
for (const field of feature.fields) {
t3.strictSame(luaDefault(source, field.name), field.default, `${field.name} default matches the lua side`);
}
t3.end();
});
}
t2.end();
});
t.test("validateFeatureValues()", t2 => {
t2.same(
validateFeatureValues("death_markers", { show_map_markers: false, collect_corpses: true }),
{ show_map_markers: false },
"values equal to their default are dropped",
);
t2.throws(() => validateFeatureValues("unknown", {}), { message: "Unknown feature unknown" });
t2.throws(
() => validateFeatureValues("death_markers", { map_icon: "x" }),
{ message: "Feature death_markers has no setting map_icon" },
);
t2.throws(
() => validateFeatureValues("death_markers", { show_map_markers: "no" }),
{ message: "Setting show_map_markers of death_markers must be a boolean" },
);
t2.throws(
() => validateFeatureValues("afk_kick", { afk_minutes: 0 }),
{ message: "Setting afk_minutes of afk_kick must be at least 1" },
);
t2.throws(
() => validateFeatureValues("afk_kick", { afk_minutes: Infinity }),
{ message: "Setting afk_minutes of afk_kick must be a finite number" },
);
t2.throws(
() => validateFeatureValues("protection", { always_protected_types: ["boiler", 1] }),
{ message: "Setting always_protected_types of protection must be a list of strings" },
);
t2.throws(
() => 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"] }),
{ always_protected_types: ["boiler"] },
"lists equal to their default are dropped",
);
t2.same(validateFeatureValues("afk_kick", { active_role_id: null }), {}, "no role is the default");
t2.same(validateFeatureValues("afk_kick", { active_role_id: 3 }), { active_role_id: 3 });
t2.same(validateFeatureValues("spawn_area", { "turrets.enabled": false }), { "turrets.enabled": false }, "nested settings");
t2.end();
});
t.test("pruneFeatureValues()", t2 => {
const afkKick = features.find(feature => feature.name === "afk_kick");
t2.same(
pruneFeatureValues(afkKick, { afk_minutes: 5, trust_time: 600, kick_minutes: "soon", active_role_id: null }),
{ kept: { afk_minutes: 5, active_role_id: null }, dropped: ["trust_time", "kick_minutes"] },
"removed settings and wrong types are dropped, valid values and optional nulls stay",
);
t2.end();
});
+123
View File
@@ -0,0 +1,123 @@
import t from "tap";
import * as lib from "@clusterio/lib";
import { Instance } from "@clusterio/host";
import { InstancePlugin } from "../dist/node/instance.js";
import { plugin as pluginDeclaration } from "../dist/node/index.js";
import * as messages from "../dist/node/messages.js";
import { features } from "../dist/node/features.js";
// The instance validates message classes against the link registry
for (const Message of pluginDeclaration.messages) {
lib.Link.register(Message);
}
const logger = { child: () => logger, info: () => {}, warn: () => {}, error: () => {}, verbose: () => {} };
class TestConnector extends lib.BaseConnector {
constructor() {
super(lib.Address.fromShorthand({ instanceId: 1 }), lib.Address.fromShorthand({ hostId: 1 }));
this.valid = true;
this.connected = true;
this.hasSession = true;
}
send() {}
}
/** Build a plugin around a real instance with spies on what leaves it. */
async function startPlugin(t2, features = [new messages.FeatureRecord("death_markers", false, { show_map_markers: false })]) {
const instanceConfig = new lib.InstanceConfig("host");
instanceConfig.set("instance.id", 1);
instanceConfig.set("instance.name", "test");
const instance = new Instance(
{ assignGamePort: () => 1 }, new TestConnector(), t2.testdir(), "factorioDir", instanceConfig
);
const state = { sent: [], rcons: [] };
instance.server = {
handle: () => {},
sendRcon: async command => {
state.rcons.push(command);
return "";
},
};
instance.sendTo = async (dst, request) => {
state.sent.push(request);
return request instanceof messages.FeatureListRequest ? features : undefined;
};
const plugin = await InstancePlugin.fromContext({ plugin: { name: "exp_scenario" }, instance, host: {}, logger });
return { plugin, instance, state };
}
/** The payload of a recorded rcon command. */
function decodeRcon(command) {
const match = command.match(/^\/sc exp_scenario\.features\.receive_update\(helpers\.json_to_table\[(=+)\[(.*)\]\1\]\)$/s);
return JSON.parse(match[2]);
}
t.test("class InstancePlugin", t2 => {
t2.test(".onStart() subscribes and sends every feature to lua", async t3 => {
const { plugin, state } = await startPlugin(t3);
await plugin.onStart();
t3.ok(state.sent[0] instanceof lib.SubscriptionRequest, "subscribes to updates first");
t3.ok(state.sent[1] instanceof messages.FeatureListRequest, "then lists the features");
const [update] = decodeRcon(state.rcons[0]);
const deathMarkers = features.find(feature => feature.name === "death_markers");
t3.equal(update.name, "death_markers");
t3.strictSame([update.values.enabled, update.values.show_map_markers], [false, false], "stored values are sent");
t3.equal(update.values.collect_corpses, true, "the rest are filled with their defaults");
t3.equal(Object.keys(update.values).length, deathMarkers.fields.length + 1, "every field and enabled");
});
t2.test(".handleFeatureUpdatedEvent() only sends while the game is up", async t3 => {
const { plugin, state } = await startPlugin(t3);
const event = new messages.FeatureUpdatedEvent([
new messages.FeatureRecord("afk_kick", true, { afk_minutes: 5 }),
new messages.FeatureRecord("removed", true, {}, 0, true),
]);
await plugin.handleFeatureUpdatedEvent(event);
t3.strictSame(state.rcons, [], "nothing is sent before start");
await plugin.onStart();
state.rcons.length = 0;
await plugin.handleFeatureUpdatedEvent(event);
const updates = decodeRcon(state.rcons[0]);
t3.strictSame(updates.map(update => update.name), ["afk_kick"], "deleted features are left out");
t3.strictSame([updates[0].values.enabled, updates[0].values.afk_minutes], [true, 5]);
await plugin.onExit();
state.rcons.length = 0;
await plugin.handleFeatureUpdatedEvent(event);
t3.strictSame(state.rcons, [], "nothing is sent after exit");
});
t2.test(".onControllerConnectionEvent() syncs again after the controller restarts", async t3 => {
const { plugin, state } = await startPlugin(t3);
await plugin.onControllerConnectionEvent("connect");
t3.strictSame(state.sent, [], "nothing happens before start");
await plugin.onStart();
state.sent.length = 0;
state.rcons.length = 0;
await plugin.onControllerConnectionEvent("resume");
t3.strictSame(state.sent, [], "a resumed session keeps its subscription");
await plugin.onControllerConnectionEvent("connect");
t3.ok(state.sent[0] instanceof lib.SubscriptionRequest, "subscribes again");
t3.equal(state.rcons.length, 1, "and sends every feature");
});
t2.test(".luaSendFeatures() picks a long bracket the values do not close", async t3 => {
const { plugin, state } = await startPlugin(t3);
const name = "a]=]b]==]c";
await plugin.luaSendFeatures([new messages.FeatureRecord("station_auto_name", true, { station_name: name })]);
t3.match(state.rcons[0], "json_to_table[===[", "uses a level not found in the json");
t3.strictSame(decodeRcon(state.rcons[0])[0].values.station_name, name);
});
t2.end();
});
+8
View File
@@ -0,0 +1,8 @@
import path from "node:path";
import t from "tap";
import { reportLuaTests } from "../../test/lua/runner.js";
const envFile = path.join(import.meta.dirname, "module", "env.lua");
// Each file runs in its own lua state, and each test in a fresh environment
t.test("features.lua", t2 => reportLuaTests(t2, envFile, path.join(import.meta.dirname, "module", "features.lua")));
+55
View File
@@ -0,0 +1,55 @@
--[[-- Test environment for module/features.lua
Extends the shared stubs with a fresh copy of the features module and a stub
of exp_util storage which lets tests act out on_init and on_load.
Run as a chunk by test/lua/runner.js, receiving the shared folder. The suite
returned here becomes `...` in each test file.
]]
local shared_root = ... --- @type string
local source = assert(debug.getinfo(1, "S")).source:gsub("\\", "/")
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 Feature ExpScenario.Features 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): ExpScenario.Feature Load module/config/<name>.lua
--- @field logged string[] Messages passed to log
--- @field events table[] Raised events, name and tick filled in
return Framework.suite(function(env)
--- @cast env ExpScenario.TestEnv
local callback --- @type fun(tbl: table)
env.logged = {}
log = function(message) env.logged[#env.logged + 1] = message end
table_size = function(tbl)
local size = 0
for _ in pairs(tbl) do size = size + 1 end
return size
end
env.extend_requires{
["modules/exp_util/storage"] = {
register = function(tbl, fn)
env.storage = tbl
callback = fn
end,
},
}
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
callback(tbl)
end
function env.load_config(name)
return assert(loadfile(plugin_root .. "/module/config/" .. name .. ".lua"))()
end
return env
end)
+152
View File
@@ -0,0 +1,152 @@
local Suite = ... --- @type Suite<ExpScenario.TestEnv>
--- The raised on_config_updated events as { path, old value, new value }, sorted by path
local function updates(env)
local rtn = {}
for _, event in ipairs(env.events) do
rtn[#rtn + 1] = { event.path, event.old_value, event.new_value }
end
table.sort(rtn, function(a, b) return a[1] < b[1] 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("register() returns the config as well", function(env)
local feature, config = env.Feature.register("test", { flag = true })
Suite.eq(config == feature.config, true, "the same table")
end)
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("update_config() sets every value, missing ones to their default", function(env)
local feature, config = env.Feature.register("test", { flag = true, count = 5, text = "a" })
feature:update_config{ enabled = false, flag = false, count = 7 }
Suite.eq(config, { enabled = false, flag = false, count = 7, text = "a" }, "given values are set")
feature:update_config{ enabled = 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 values are kept in storage")
end)
Suite.test("update_config() raises on_config_updated for each changed value", function(env)
local feature = env.Feature.register("test", { flag = true, names = env.Feature.set{ "a" }, section = { count = 1 } })
feature:update_config{ enabled = false, flag = true, names = { "a" }, ["section.count"] = 2 }
Suite.eq(env.Feature.events.on_config_updated, assert(env.events[1]).name, "the event is on_config_updated")
Suite.eq(updates(env), {
{ "enabled", true, false },
{ "section.count", 1, 2 },
}, "with the path, old value, and new value, unchanged values raise nothing")
end)
Suite.test("update_config() raises when a value goes back to its default", function(env)
local feature = env.Feature.register("test", { flag = true })
feature:update_config{ enabled = true, flag = false }
env.events = {}
feature:update_config{ enabled = true }
Suite.eq(updates(env), { { "flag", false, true } }, "the value reverts")
end)
Suite.test("update_config() ignores and logs values of the wrong type", function(env)
local function check() return true end
local feature, config = env.Feature.register("test", { flag = true, check = check })
feature:update_config{ enabled = true, flag = "yes", check = false }
Suite.eq(config, { enabled = true, flag = true, check = check }, "wrong types and functions keep their default")
Suite.eq(#env.logged, 2, "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", values = { enabled = true, flag = false } },
{ name = "unknown", values = { enabled = false } },
}
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 stored values without raising events", function(env)
local feature, config = env.Feature.register("test", { flag = true })
env.on_load{ test = { enabled = false, flag = false, gone = 1 } }
Suite.eq(config, { enabled = false, flag = false }, "the saved values apply, unknown ones are skipped")
Suite.empty(env.events, "nothing is raised")
feature:update_config{ enabled = true }
Suite.eq(env.storage, { test = { enabled = true } }, "later updates replace the stored values")
end)
Suite.test("guard() skips handlers while the feature is disabled", function(env)
local feature = env.Feature.register("test")
local calls = {}
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")
feature:update_config{ enabled = false }
lib.events.on_event("c")
lib.on_nth_tick[60]("d")
Suite.eq(calls, { "a", "b" }, "only called while enabled")
end)
Suite.test("register() addresses nested values by path", function(env)
local offset = { 1, 2 }
local feature, config = env.Feature.register("test", { section = { flag = true, count = 1 }, offset = offset })
feature:update_config{ enabled = true, ["section.flag"] = false, offset = { "x" } }
Suite.eq(config.section, { flag = false, count = 1 }, "the nested value changes in place")
Suite.eq(config.offset, offset, "tables which are not sections can not be set")
feature:update_config{ enabled = true }
Suite.eq(config.section.flag, true, "and goes back to its default")
end)
Suite.test("list() declares a list of strings", function(env)
local feature, config = env.Feature.register("test", { names = env.Feature.list{ "a", "b" }, empty = env.Feature.list{}, plain = { "x" } })
Suite.eq(config.names, { "a", "b" }, "the default is the list")
feature:update_config{ enabled = true, names = { "c" }, empty = { "d" }, plain = { "y" } }
Suite.eq(config.names, { "c" }, "a list is replaced")
Suite.eq(config.empty, { "d" }, "an empty list can be set")
Suite.eq(config.plain, { "x" }, "a plain table can not be set")
feature:update_config{ enabled = true, names = { 1 } }
Suite.eq(config.names, { "a", "b" }, "a list with other types is invalid")
end)
Suite.test("set() declares a set of strings which is sent as a list", function(env)
local feature, config = env.Feature.register("test", { names = env.Feature.set{ "a" }, empty = env.Feature.set{} })
Suite.eq(config.names, { a = true }, "the default is a set")
Suite.eq(config.empty, {}, "and can be empty")
feature:update_config{ enabled = true, names = { "b", "c" } }
Suite.eq(config.names, { b = true, c = true }, "the list is converted to a set")
env.on_load{ test = { enabled = true, names = { "d" } } }
Suite.eq(config.names, { d = true }, "also when the values are loaded")
end)
Suite.test("optional() declares a value without a default", function(env)
local feature, config = env.Feature.register("test", { role_id = env.Feature.optional("number") })
Suite.eq(config.role_id, nil, "nil until set")
feature:update_config{ enabled = true, role_id = 3 }
Suite.eq(config.role_id, 3, "set with the given type")
feature:update_config{ enabled = true }
Suite.eq(config.role_id, nil, "and back to nil")
end)
Suite.test("config files register their feature", function(env)
local feature = env.load_config("spawn_area")
feature:update_config{ enabled = true, ["turrets.enabled"] = false }
Suite.eq(feature.config.turrets.enabled, false, "spawn_area can be changed")
Suite.eq(feature.config.turrets.ammo_type, "uranium-rounds-magazine", "other values keep their default")
end)
return Suite.run()
Loaded 100 of 104 files, more files were not shown because too many files have changed in this diff. Show more