From 90d63952d3ba9747df0a58a42fb69e5434f17fdb Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Wed, 20 May 2026 01:48:37 +0100 Subject: [PATCH] Add lua module code --- exp_groups/module/control.lua | 436 ++++++++++++++++++++++++++++++++++ 1 file changed, 436 insertions(+) create mode 100644 exp_groups/module/control.lua diff --git a/exp_groups/module/control.lua b/exp_groups/module/control.lua new file mode 100644 index 00000000..831e4ea6 --- /dev/null +++ b/exp_groups/module/control.lua @@ -0,0 +1,436 @@ +--[[-- Exp Permission Groups +Adds permission group syncing to clusterio +]] + +local clusterio_api = require("modules/clusterio/api") +local compat = require("modules/clusterio/compat") + +--- Top level module table, contains event handlers and public methods +--- @class ExpPermissionGroups +local ExpPermissionGroups = {} + +--- @class ExpPermissionGroups.GroupPermissions +--- @field [1] boolean -- is_blacklist, true means action names is a backlist +--- @field [2] string[] -- action_names, the actions to allow or disallow + +--- @class ExpPermissionGroups.GroupRecord +--- @field id number +--- @field name string +--- @field permissions ExpPermissionGroups.GroupPermissions? +--- @field isDeleted boolean + +--- @class ExpPermissionGroups.AssignmentRecord +--- @field name string +--- @field groupId number +--- @field isDeleted boolean + +--- @class ExpPermissionGroups.ScriptData +--- @field factorio_to_clusterio_id table +--- @field clusterio_id_to_group table +--- @field dirty_groups table +--- @field dirty_players table +--- @field emit_updates boolean +local script_data = {} + +local function setup_script_data() + if compat.script_data["exp_groups"] == nil then + --- @type ExpPermissionGroups.ScriptData + compat.script_data["exp_groups"] = { + factorio_to_clusterio_id = {}, + clusterio_id_to_group = {}, + dirty_groups = {}, + dirty_players = {}, + emit_updates = false, + } + end + + ExpPermissionGroups.on_load() +end + +--[[ + Helper methods +]] + +--- Get the default factorio permission group +--- @return LuaPermissionGroup +local function get_default_group() + return assert(game.permissions.get_group("Default")) +end + +--- Move all players from one group to another +--- @param group_src LuaPermissionGroup +--- @param group_dst LuaPermissionGroup +local function move_players(group_src, group_dst) + local add_player = group_dst.add_player + for _, player in pairs(group_src.players) do + add_player(player) + end +end + +--- Apply an encoded permission definition to a group +--- @param group LuaPermissionGroup +--- @param permissions ExpPermissionGroups.GroupPermissions +local function decode_group_permissions(group, permissions) + local is_blacklist = permissions[1] + local actions_names = permissions[2] + local action_allowed = not is_blacklist + + -- Construct a hash map for faster lookup + local action_map = {} + for _, input_action_name in pairs(actions_names) do + action_map[input_action_name] = true + end + + -- Apply the whitelist / backlist to the group + for input_action_name, input_action in pairs(defines.input_action) do + if action_map[input_action_name] then + group.set_allows_action(input_action, action_allowed) + else + group.set_allows_action(input_action, is_blacklist) + end + end +end + +--- Encode the permissions of a group in a shorthand format +--- @param group LuaPermissionGroup +--- @return ExpPermissionGroups.GroupPermissions +local function encode_group_permissions(group) + local whitelist = {} --- @type string[] + local blacklist = {} --- @type string[] + local whitelist_index = 0 + local blacklist_index = 0 + + -- Construct the whitelist and blacklist + local allows_action = group.allows_action + for input_action_name, input_action in pairs(defines.input_action) do + if allows_action(input_action) then + whitelist[whitelist_index] = input_action_name + whitelist_index = whitelist_index + 1 + else + blacklist[blacklist_index] = input_action_name + blacklist_index = blacklist_index + 1 + end + end + + -- Return the whitelist if it is smaller + if blacklist_index > whitelist_index then + return { false, whitelist } + end + + -- Otherwise return the blacklist as it is smaller + return { true, blacklist } +end + +--[[ + State handlers +]] + +--- Update the factorio permission group +--- @param group_record ExpPermissionGroups.GroupRecord +local function update_group(group_record) + assert(not group_record.isDeleted) + + -- Try find the group by id and then then name + local group = script_data.clusterio_id_to_group[group_record.id] + if not group then + group = game.permissions.get_group(group_record.name) + end + + -- Create a new group or update the found group + if not group or not group.valid then + group = assert(game.permissions.create_group(group_record.name)) + else + group.name = group_record.name + end + + -- Update the permissions for the group + if group_record.permissions then + decode_group_permissions(group, group_record.permissions) + end + + -- Update the script data + script_data.factorio_to_clusterio_id[group.group_id] = group_record.id + script_data.clusterio_id_to_group[group_record.id] = group +end + +--- Delete the factorio permission group +--- @param group_record ExpPermissionGroups.GroupRecord +local function delete_group(group_record) + assert(group_record.isDeleted) + + local default_group = get_default_group() + local group = script_data.clusterio_id_to_group[group_record.id] + if group then + move_players(group, default_group) + group.destroy() + end +end + +--- Update an assignment by moving the player to their new group +--- @param assignment_record ExpPermissionGroups.AssignmentRecord +local function update_assignment(assignment_record) + assert(not assignment_record.isDeleted) + + local group = script_data.clusterio_id_to_group[assignment_record.groupId] + local player = assert(game.get_player(assignment_record.name)) + if group then + group.add_player(player) + end +end + +--- Clear an assignment by moving the player to the default group +--- @param assignment_record ExpPermissionGroups.AssignmentRecord +local function delete_assignment(assignment_record) + assert(assignment_record.isDeleted) + + local default_group = get_default_group() + local player = assert(game.get_player(assignment_record.name)) + default_group.add_player(player) +end + +--[[ + Public methods +]] + +--- Restore local references to persistent script data after load +function ExpPermissionGroups.on_load() + script_data = compat.script_data["exp_groups"] +end + +--- Enable or disable emitting lua changes back to the instance plugin +--- @param enabled boolean? +function ExpPermissionGroups.set_emit_events(enabled) + script_data.emit_updates = enabled ~= false +end + +--- Replace local state with expected controller state on startup +--- @param group_records ExpPermissionGroups.GroupRecord[] +function ExpPermissionGroups.initialise_groups(group_records) + local _emit_events = script_data.emit_updates + script_data.emit_updates = false + + -- Update all the received groups + local seen_clusterio_ids = {} --- @type table + for _, group_record in pairs(group_records) do + update_group(group_record) + seen_clusterio_ids[group_record.id] = true + end + + -- Cleanup the script data, removing stale group ids + local factorio_to_clusterio_id = script_data.factorio_to_clusterio_id + local clusterio_id_to_group = script_data.clusterio_id_to_group + for factorio_id, clusterio_id in pairs(factorio_to_clusterio_id) do + if not seen_clusterio_ids[clusterio_id] then + factorio_to_clusterio_id[factorio_id] = nil + clusterio_id_to_group[clusterio_id] = nil + end + end + + -- Remove all other groups + local default_group = get_default_group() + local default_group_id = default_group.group_id + for _, group in pairs(game.permissions.groups) do + if not factorio_to_clusterio_id[group.group_id] and group.group_id ~= default_group_id then + move_players(group, default_group) + group.destroy() + end + end + + script_data.emit_updates = _emit_events +end + +--- Receive an updated version of a group record +--- @param group_record ExpPermissionGroups.GroupRecord +function ExpPermissionGroups.receive_group_update(group_record) + local _emit_events = script_data.emit_updates + script_data.emit_updates = false + + if group_record.isDeleted then + delete_group(group_record) + else + update_group(group_record) + end + + script_data.emit_updates = _emit_events +end + +--- Receive an updated version of a assignment record +--- @param assignment_record ExpPermissionGroups.AssignmentRecord +function ExpPermissionGroups.receive_assignment_update(assignment_record) + local _emit_events = script_data.emit_updates + script_data.emit_updates = false + + if assignment_record.isDeleted then + delete_assignment(assignment_record) + else + update_assignment(assignment_record) + end + + script_data.emit_updates = _emit_events +end + +--- Get the current script data for debugging purposes +--- @package +function ExpPermissionGroups._script_data() + return script_data +end + +--[[ + IPC events +]] + +--- Emit a group update to the instance plugin +--- @param group LuaPermissionGroup +local function emit_group_update(group) + clusterio_api.send_json("exp_group:group_updated", { + group_name = group.name, + group_id = script_data.factorio_to_clusterio_id[group.group_id], + permissions = encode_group_permissions(group), + }) +end + +--- Emit a group deletion to the instance plugin +--- @param group_id number +--- @param group_name string +local function emit_group_delete(group_id, group_name) + clusterio_api.send_json("exp_group:group_deleted", { + group_name = group_name, + group_id = script_data.factorio_to_clusterio_id[group_id], + }) +end + +--- Emit a player assignment to the instance plugin +--- @param assignments table +local function emit_player_assignments(assignments) + clusterio_api.send_json("exp_group:player_assignments", { + assignments = assignments + }) +end + +--[[ + IPC event queuing +]] + +--- Mark a group as changed +--- @param group LuaPermissionGroup +local function mark_group_dirty(group) + script_data.dirty_groups[group.group_id] = group +end + +--- Mark a player as changed +--- @param player LuaPlayer +local function mark_player_dirty(player) + script_data.dirty_players[player.name] = player +end + +--- Flush queued group updates to the instance plugin +local function flush_group_updates() + -- Check if updates should be updated + if not script_data.emit_updates then + script_data.dirty_groups = {} + return + end + + -- Get all the groups and emit their updates + local get_group = game.permissions.get_group + for _, group in pairs(script_data.dirty_groups) do + if group.valid then + emit_group_update(group) + end + end + + script_data.dirty_groups = {} +end + +--- Flush queued player updates to the instance plugin +local function flush_player_updates() + -- Check if updates should be updated + if not script_data.emit_updates then + script_data.dirty_players = {} + return + end + + -- Construct the update payload + local assignments = {} --- @type table + for player_name, player in pairs(script_data.dirty_players) do + local group = player.valid and player.permission_group + if group then + assignments[player_name] = script_data.factorio_to_clusterio_id[group.group_id] + end + end + + emit_player_assignments(assignments) + + script_data.dirty_players = {} +end + +--[[ + Factorio events +]] + +--- Handle clusterio server startup +local function on_server_startup() + setup_script_data() +end + +--- Handle creation of permission groups +--- @param event EventData.on_permission_group_added +local function on_permission_group_added(event) + -- Check if updates should be updated + if not script_data.emit_updates then + return + end + + emit_group_update(event.group) +end + +--- Handle deletion of permission groups +--- @param event EventData.on_permission_group_deleted +local function on_permission_group_deleted(event) + -- Check if updates should be updated + if not script_data.emit_updates then + return + end + + emit_group_delete(event.id, event.group_name) +end + +--- Handle edits make to permission groups +--- @param event EventData.on_permission_group_edited +local function on_permission_group_edited(event) + -- Check if updates should be updated + if not script_data.emit_updates then + return + end + + -- Check if this is a player or group event + if event.type == "add-player" or event.type == "remove-player" then + local player = assert(game.get_player(event.other_player_index)) + mark_player_dirty(player) + else + mark_group_dirty(event.group) + end +end + +--- Periodically flush queued changes +local function on_nth_tick_flush() + flush_group_updates() + flush_player_updates() +end + +local e = defines.events + +local events = { + [clusterio_api.events.on_server_startup] = on_server_startup, + [e.on_permission_group_added] = on_permission_group_added, + [e.on_permission_group_deleted] = on_permission_group_deleted, + [e.on_permission_group_edited] = on_permission_group_edited, +} + +local on_nth_tick = { + [300] = on_nth_tick_flush, +} + +ExpPermissionGroups.events = events --- @package +ExpPermissionGroups.on_nth_tick = on_nth_tick --- @package +return ExpPermissionGroups