mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2026-11-12 23:54:53 +09:00
Merge branch 'explosivegaming:main' into translate
This commit is contained in:
104 files changed
+4673
-2819
No files matched your search
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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 = {
|
||||
},
|
||||
}
|
||||
@@ -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"] },
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
return {
|
||||
modules = {
|
||||
["forcestats"] = true,
|
||||
["logistorage"] = false,
|
||||
["other"] = true,
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
--- Settings for lawnmower
|
||||
-- @config lawnmower
|
||||
|
||||
return {
|
||||
destroy_decoratives = false,
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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__",
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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",
|
||||
],
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
+8
-9
@@ -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,
|
||||
},
|
||||
|
||||
}
|
||||
+21
-31
@@ -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
|
||||
+35
-35
@@ -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 },
|
||||
},
|
||||
}
|
||||
})
|
||||
+31
-40
@@ -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
|
||||
]]
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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"),
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
+4
-6
@@ -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")
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"load": [
|
||||
],
|
||||
"require": [
|
||||
"globals.lua",
|
||||
"control.lua"
|
||||
],
|
||||
"dependencies": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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")));
|
||||
@@ -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)
|
||||
@@ -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
Reference in new issue
Block a user