Files
factorio-scenario-ExpCluster/exp_util/module/selection.lua
Cooldude2606 27a2feaae9 Refactor Selection Util (#409)
* Add Selection to ExpUtil

* Convert modules to use new lib

* Bug Fixes
2025-09-04 10:54:55 +01:00

254 lines
8.4 KiB
Lua

--[[-- ExpUtil - Selection
Provides an easy way for working with selection planners
]]
local ExpUtil = require("modules/exp_util")
--- @class Selection.Active
--- @field name string
--- @field character LuaEntity?
--- @field data table
--- @alias Selection.event_handler<E> fun(event: E, ...: any)
--- @class ExpUtil_Selection
local Selection = {
on_selection_start = script.generate_event_name(),
--- @class EventData.on_selection_start: EventData
--- @field player_index number
--- @field selection Selection.Active
on_selection_stop = script.generate_event_name(),
--- @class EventData.on_selection_stop: EventData
--- @field player_index number
--- @field selection Selection.Active
--- @type table<string, { [defines.events]: Selection.event_handler[] }>
_registered = {},
--- @package
events = {},
}
--- @class Selection
--- @field name string
--- @field _handlers table
Selection._prototype = {}
Selection._metatable = {
__index = Selection._prototype,
__class = "Selection",
}
--- @type table<number, Selection.Active>
local script_data = {}
local Storage = require("modules/exp_util/storage")
Storage.register(script_data, function(tbl)
script_data = tbl
end)
local empty_table = {}
local selection_tool = { name = "selection-tool" }
--- Test if a player is holding a selection tool
--- @param player LuaPlayer
--- @return boolean?
local function has_selection_tool_in_hand(player)
return player.cursor_stack and player.cursor_stack.valid_for_read and player.cursor_stack.name == "selection-tool"
end
--- Give a selection tool to a player if they don't have one
--- @param player LuaPlayer
local function give_selection_tool(player)
if has_selection_tool_in_hand(player) then return end
player.clear_cursor()
player.cursor_stack.set_stack(selection_tool)
-- This does not work for selection planners, will make a feature request for it
--player.cursor_stack_temporary = true
-- Make a slot to place the selection tool even if inventory is full
if player.character then
player.character_inventory_slots_bonus = player.character_inventory_slots_bonus + 1
end
local inventory = player.get_main_inventory()
if inventory then
player.hand_location = { inventory = inventory.index, slot = #inventory }
end
end
--- Remove a selection tool to a player if they have one
--- @param player LuaPlayer
--- @param old_character LuaEntity?
local function remove_selection_tool(player, old_character)
-- Remove the selection tool
if has_selection_tool_in_hand(player) then
player.cursor_stack.clear()
else
player.remove_item(selection_tool)
end
-- Remove the extra slot
if old_character and old_character == player.character then
player.character_inventory_slots_bonus = player.character_inventory_slots_bonus - 1
player.hand_location = nil
end
end
--- Create a connection from which events can be registered
--- @param name string
--- @return Selection
function Selection.connect(name)
ExpUtil.assert_not_runtime()
ExpUtil.assert_argument_type(name, "string", 1, "name")
local handlers = Selection._registered[name] or {}
Selection._registered[name] = handlers
return setmetatable({
name = name,
_handlers = handlers,
}, Selection._metatable)
end
--- Stop the currently active selection for a player
--- @param player LuaPlayer
function Selection.stop(player)
local player_index = player.index
local active_selection = script_data[player_index]
if not active_selection then
return
end
remove_selection_tool(player, active_selection.character)
script_data[player_index] = nil
script.raise_event(Selection.on_selection_stop, {
player_index = player_index,
selection = active_selection,
})
end
--- Start a new selection for a player
--- @param player LuaPlayer
--- @param ... unknown
function Selection._prototype:start(player, ...)
local player_index = player.index
local active_selection = script_data[player_index]
if active_selection then
script.raise_event(Selection.on_selection_stop, {
player_index = player_index,
selection = active_selection,
})
end
local selection = {
name = self.name,
character = player.character,
data = select("#", ...) > 0 and { ... } or empty_table,
}
give_selection_tool(player)
script_data[player_index] = selection
script.raise_event(Selection.on_selection_start, {
player_index = player_index,
selection = selection,
})
end
--- Stop this selection if it is active, returns if this selection was active
--- @param player LuaPlayer
--- @return boolean
function Selection._prototype:stop(player)
local player_index = player.index
local active_selection = script_data[player_index]
if not active_selection or active_selection.name ~= self.name then
return false
end
remove_selection_tool(player, active_selection.character)
script_data[player_index] = nil
script.raise_event(Selection.on_selection_stop, {
player_index = player_index,
selection = active_selection,
})
return true
end
--- Dispatch events to the correct handlers
--- @param event EventData.on_player_selected_area | EventData.on_player_alt_selected_area | EventData.on_selection_start | EventData.on_selection_stop
local function event_dispatch(event)
local active_selection = event.selection or script_data[event.player_index]
local selection_handlers = active_selection and Selection._registered[active_selection.name]
local handlers = selection_handlers and selection_handlers[event.name] or empty_table
for _, handler in ipairs(handlers) do
handler(event, table.unpack(active_selection.data))
end
end
--- Create an on event adder
--- @param event defines.events
--- @return function
local function on_event_factory(event)
return function(self, callback)
ExpUtil.assert_not_runtime()
ExpUtil.assert_argument_type(callback, "function", 1, "callback")
Selection.events[event] = event_dispatch
local handlers = self._handlers[event] or {}
handlers[#handlers + 1] = callback
self._handlers[event] = handlers
return self
end
end
local e = defines.events
--- @alias Selection.on_event<E> fun(self: Selection, callback: fun(event: E, ...: any)): Selection
--- @type Selection.on_event<EventData.on_selection_start>
Selection._prototype.on_start = on_event_factory(Selection.on_selection_start)
--- @type Selection.on_event<EventData.on_selection_stop>
Selection._prototype.on_stop = on_event_factory(Selection.on_selection_stop)
--- @type Selection.on_event<EventData.on_player_selected_area>
Selection._prototype.on_selection = on_event_factory(e.on_player_selected_area)
--- @type Selection.on_event<EventData.on_player_alt_selected_area>
Selection._prototype.on_alt_selection = on_event_factory(e.on_player_alt_selected_area)
--- Stop selection if the selection tool is removed from the cursor
--- @param event EventData.on_player_cursor_stack_changed
Selection.events[e.on_player_cursor_stack_changed] = function(event)
local player = assert(game.get_player(event.player_index))
if has_selection_tool_in_hand(player) then return end
Selection.stop(player)
end
--- Make sure the hand location exists when the player returns from remote view
--- @param event EventData.on_player_controller_changed
Selection.events[e.on_player_controller_changed] = function(event)
local player = assert(game.get_player(event.player_index))
local inventory = player.get_main_inventory()
if inventory and has_selection_tool_in_hand(player) then
player.hand_location = { inventory = inventory.index, slot = #inventory }
end
end
--- Stop selection after an event
--- @param event EventData.on_pre_player_left_game | EventData.on_pre_player_died
local function stop_after_event(event)
local player = assert(game.get_player(event.player_index))
Selection.stop(player)
end
Selection.events[e.on_pre_player_left_game] = stop_after_event
Selection.events[e.on_pre_player_died] = stop_after_event
return Selection