From dca5086ce375ef592bee153b72eca18946c476c9 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Wed, 20 May 2026 22:39:44 +0100 Subject: [PATCH] Add instance plugin --- exp_groups/instance.ts | 149 +++++++++++++++++++++++++++++++++- exp_groups/module/control.lua | 51 ++++++------ exp_groups/module/globals.lua | 9 ++ 3 files changed, 181 insertions(+), 28 deletions(-) create mode 100644 exp_groups/module/globals.lua diff --git a/exp_groups/instance.ts b/exp_groups/instance.ts index 455d035c..028663b9 100644 --- a/exp_groups/instance.ts +++ b/exp_groups/instance.ts @@ -1,6 +1,153 @@ -import * as lib from "@clusterio/lib"; import { BaseInstancePlugin } from "@clusterio/host"; +import * as lib from "@clusterio/lib"; +import * as messages from "./messages"; + +export type IpcGroupUpdated = { + group_name: string, + group_id: number | undefined, + permissions: { is_blacklist: boolean, permissions: string[] }, +}; + +export type IpcGroupDeleted = { + group_name: string, + group_id: number | undefined, +}; + +export type IpcPlayerAssignments = { + assignments: Record, +}; export class InstancePlugin extends BaseInstancePlugin { + async init() { + this.instance.handle(messages.GroupUpdatedEvent, this.handleGroupUpdatedEvent.bind(this)); + this.instance.handle(messages.AssignmentUpdatedEvent, this.handleAssignmentUpdatedEvent.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)) + } + async onInstanceConfigFieldChanged(field: string, curr: unknown, prev: unknown) { + switch(field) { + case "exp_groups.sync_mode": + await this.luaSetEmitEvents(curr == "bidirectional") + break; + } + } + + async onStart() { + await this.instance.sendTo("controller", new lib.SubscriptionRequest( + `exp_groups:${messages.GroupUpdatedEvent.name}`, true + )); + const groups = await this.instance.sendTo("controller", new messages.GroupListRequest()) + await this.luaSendInitialGroups(groups); + await this.luaSetEmitEvents(this.instance.config.get("exp_groups.sync_mode") == "bidirectional") + } + + async onPlayerEvent(event: lib.PlayerEvent) { + switch(event.type) { + case "join": + this.handlePlayerJoin(event.name); + break; + case "leave": + this.handlePlayerLeave(event.name); + break; + } + } + + async handlePlayerJoin(playerName: string) { + await this.subscribePlayerAssignment(playerName); + await this.requestPlayerAssignment(playerName); + } + + async handlePlayerLeave(playerName: string) { + await this.unsubscribePlayerAssignment(playerName); + } + + async handleGroupUpdatedEvent(event: messages.GroupUpdatedEvent) { + for (const group of event.updates) { + await this.luaSendGroupUpdate(group); + } + } + + async handleAssignmentUpdatedEvent(event: messages.AssignmentUpdatedEvent) { + for (const assignment of event.updates) { + await this.luaSendAssignmentUpdate(assignment); + } + } + + async handleGroupUpdatedIPC(event: IpcGroupUpdated) { + const permissions = new messages.GroupPermissions( + event.permissions.is_blacklist, + event.permissions.permissions, + ); + + if (event.group_id === undefined) { + await this.instance.sendTo("controller", + new messages.GroupCreateRequest(event.group_name, permissions), + ); + } else { + await this.instance.sendTo("controller", new messages.GroupUpdateRequest( + new messages.GroupRecord(event.group_id, event.group_name, permissions), + )); + } + } + + async handleGroupDeletedIPC(event: IpcGroupDeleted) { + if (event.group_id === undefined) { + return; + } + await this.instance.sendTo("controller", new messages.GroupDeleteRequest(event.group_id)); + } + + async handlePlayerAssignmentsIPC(event: IpcPlayerAssignments) { + await Promise.all( + Object.entries(event.assignments).map(([playerName, groupId]) => + this.instance.sendTo("controller", + new messages.AssignmentUpdateRequest(new messages.AssignmentRecord(playerName, groupId)), + ) + ) + ); + } + + async subscribePlayerAssignment(playerName: string) { + await this.instance.sendTo("controller", new lib.SubscriptionRequest( + `exp_groups:${messages.AssignmentUpdatedEvent.name}`, true, 0, playerName + )); + } + + async unsubscribePlayerAssignment(playerName: string) { + await this.instance.sendTo("controller", new lib.SubscriptionRequest( + `exp_groups:${messages.AssignmentUpdatedEvent.name}`, false, 0, playerName + )); + } + + async requestPlayerAssignment(playerName: string) { + const assignment = await this.instance.sendTo("controller", new messages.AssignmentGetRequest(playerName)); + await this.luaSendAssignmentUpdate(assignment); + } + + async luaSendInitialGroups(groups: messages.GroupRecord[]) { + if (this.instance.config.get("exp_groups.sync_mode") == "disabled") { + return; + } + await this.instance.sendRcon(`/sc exp_groups.initialise_groups(helpers.json_to_table${JSON.stringify(groups)})`) + } + + async luaSendGroupUpdate(group: messages.GroupRecord) { + if (this.instance.config.get("exp_groups.sync_mode") == "disabled") { + return; + } + await this.instance.sendRcon(`/sc exp_groups.receive_group_update(helpers.json_to_table${JSON.stringify(group)})`) + } + + async luaSendAssignmentUpdate(assignment: messages.AssignmentRecord) { + if (this.instance.config.get("exp_groups.sync_mode") == "disabled") { + return; + } + await this.instance.sendRcon(`/sc exp_groups.initialise_groups(helpers.receive_assignment_update${JSON.stringify(assignment)})`) + } + + async luaSetEmitEvents(emitEvents: boolean) { + await this.instance.sendRcon(`/sc exp_groups.set_emit_events(${emitEvents})`) + } } diff --git a/exp_groups/module/control.lua b/exp_groups/module/control.lua index 831e4ea6..2a854bdb 100644 --- a/exp_groups/module/control.lua +++ b/exp_groups/module/control.lua @@ -1,4 +1,4 @@ ---[[-- Exp Permission Groups +--[[-- ExpGroups Adds permission group syncing to clusterio ]] @@ -6,18 +6,18 @@ 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 ExpGroups +local ExpGroups = {} --- @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 +--- @field is_blacklist boolean +--- @field permissions string[] --- @class ExpPermissionGroups.GroupRecord --- @field id number --- @field name string --- @field permissions ExpPermissionGroups.GroupPermissions? ---- @field isDeleted boolean +--- @field is_deleted boolean --- @class ExpPermissionGroups.AssignmentRecord --- @field name string @@ -44,7 +44,7 @@ local function setup_script_data() } end - ExpPermissionGroups.on_load() + ExpGroups.on_load() end --[[ @@ -71,17 +71,15 @@ end --- @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 + for _, input_action_name in pairs(permissions.permissions) do action_map[input_action_name] = true end -- Apply the whitelist / backlist to the group + local is_blacklist = permissions.is_blacklist + local action_allowed = not is_blacklist 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) @@ -114,11 +112,11 @@ local function encode_group_permissions(group) -- Return the whitelist if it is smaller if blacklist_index > whitelist_index then - return { false, whitelist } + return { is_blacklist = false, permissions = whitelist } end -- Otherwise return the blacklist as it is smaller - return { true, blacklist } + return { is_blacklist = true, permissions = blacklist } end --[[ @@ -128,7 +126,7 @@ end --- Update the factorio permission group --- @param group_record ExpPermissionGroups.GroupRecord local function update_group(group_record) - assert(not group_record.isDeleted) + assert(not group_record.is_deleted) -- Try find the group by id and then then name local group = script_data.clusterio_id_to_group[group_record.id] @@ -156,7 +154,7 @@ end --- Delete the factorio permission group --- @param group_record ExpPermissionGroups.GroupRecord local function delete_group(group_record) - assert(group_record.isDeleted) + assert(group_record.is_deleted) local default_group = get_default_group() local group = script_data.clusterio_id_to_group[group_record.id] @@ -193,19 +191,19 @@ end ]] --- Restore local references to persistent script data after load -function ExpPermissionGroups.on_load() +function ExpGroups.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) +function ExpGroups.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) +function ExpGroups.initialise_groups(group_records) local _emit_events = script_data.emit_updates script_data.emit_updates = false @@ -241,11 +239,11 @@ end --- Receive an updated version of a group record --- @param group_record ExpPermissionGroups.GroupRecord -function ExpPermissionGroups.receive_group_update(group_record) +function ExpGroups.receive_group_update(group_record) local _emit_events = script_data.emit_updates script_data.emit_updates = false - if group_record.isDeleted then + if group_record.is_deleted then delete_group(group_record) else update_group(group_record) @@ -256,7 +254,7 @@ end --- Receive an updated version of a assignment record --- @param assignment_record ExpPermissionGroups.AssignmentRecord -function ExpPermissionGroups.receive_assignment_update(assignment_record) +function ExpGroups.receive_assignment_update(assignment_record) local _emit_events = script_data.emit_updates script_data.emit_updates = false @@ -271,7 +269,7 @@ end --- Get the current script data for debugging purposes --- @package -function ExpPermissionGroups._script_data() +function ExpGroups._script_data() return script_data end @@ -332,7 +330,6 @@ local function flush_group_updates() 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) @@ -431,6 +428,6 @@ local on_nth_tick = { [300] = on_nth_tick_flush, } -ExpPermissionGroups.events = events --- @package -ExpPermissionGroups.on_nth_tick = on_nth_tick --- @package -return ExpPermissionGroups +ExpGroups.events = events --- @package +ExpGroups.on_nth_tick = on_nth_tick --- @package +return ExpGroups diff --git a/exp_groups/module/globals.lua b/exp_groups/module/globals.lua new file mode 100644 index 00000000..fc42b917 --- /dev/null +++ b/exp_groups/module/globals.lua @@ -0,0 +1,9 @@ +--[[ +It is best practice to not expose any globals because all modules share a global environment +However, sometimes you need globals, for example to access functions within rcon commands +Therefore, we advise that this should be the only file in your module to expose globals +Typically this would be your control file as shown in the example below +]] + +-- Access using `/sc exp_groups.foo()` +exp_groups = require("modules/exp_groups/control")