diff --git a/exp_legacy/module/modules/control/selection.lua b/exp_legacy/module/modules/control/selection.lua deleted file mode 100644 index 461f2cd4..00000000 --- a/exp_legacy/module/modules/control/selection.lua +++ /dev/null @@ -1,194 +0,0 @@ ---[[-- Control Module - Selection - - Controls players who have a selection planner, mostly event handlers - @control Selection - @alias Selection -]] - -local Event = require("modules/exp_legacy/utils/event") --- @dep utils.event -local Storage = require("modules/exp_util/storage") -local Selection = { - events = { - --- When a player enters selection mode - -- @event on_player_selection_start - -- @tparam number player_index the player index of the player who entered selection mode - -- @tparam string selection the name of the selection being made - on_player_selection_start = script.generate_event_name(), - --- When a player leaves selection mode - -- @event on_player_selection_end - -- @tparam number player_index the player index of the player who left selection mode - -- @tparam string selection the name of the selection which ended - on_player_selection_end = script.generate_event_name(), - }, -} - -local selection_tool = { name = "selection-tool" } - -local selections = {} -Storage.register({ - selections = selections, -}, function(tbl) - selections = tbl.selections -end) - -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 - ---- Let a player select an area by providing a selection planner ---- @param player LuaPlayer The player to place into selection mode ---- @param selection_name string The name of the selection to start, used with on_selection ---- @param single_use boolean? When true the selection will stop after first use ---- @param ... any Arguments to pass to the selection handler -function Selection.start(player, selection_name, single_use, ...) - if not player or not player.valid or not player.cursor_stack then return end - if selections[player.index] then - -- Raise the end event if a selection was already in progress - script.raise_event(Selection.events.on_player_selection_end, { - name = Selection.events.on_player_selection_end, - tick = game.tick, - player_index = player.index, - selection = selections[player.index].name, - }) - end - - -- Set the selection data - selections[player.index] = { - name = selection_name, - arguments = { ... }, - single_use = single_use == true, - character = player.character, - } - - -- Raise the event - script.raise_event(Selection.events.on_player_selection_start, { - name = Selection.events.on_player_selection_start, - tick = game.tick, - player_index = player.index, - selection = selection_name, - }) - - -- Give a selection tool if one is not in use - if has_selection_tool_in_hand(player) then return end - player.clear_cursor() -- Clear the current item - 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 - ---- Stop a player selection by removing the selection planner --- @tparam LuaPlayer player The player to exit out of selection mode -function Selection.stop(player) - if not selections[player.index] then return end - local character = selections[player.index].character - local selection = selections[player.index].name - selections[player.index] = nil - - -- Raise the event - script.raise_event(Selection.events.on_player_selection_end, { - name = Selection.events.on_player_selection_end, - tick = game.tick, - player_index = player.index, - selection = selection, - }) - - -- 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 character and character == player.character then - player.character_inventory_slots_bonus = player.character_inventory_slots_bonus - 1 - player.hand_location = nil - end -end - ---- Get the selection arguments for a player --- @tparam LuaPlayer player The player to get the selection arguments for -function Selection.get_arguments(player) - if not selections[player.index] then return end - return selections[player.index].arguments -end - ---- Test if a player is selecting something --- @tparam LuaPlayer player The player to test --- @tparam[opt] string selection_name If given will only return true if the selection is this selection -function Selection.is_selecting(player, selection_name) - if selection_name ~= nil then - if not selections[player.index] then return false end - return selections[player.index].name == selection_name - else - return has_selection_tool_in_hand(player) - end -end - ---- Filter on_player_selected_area to this custom selection, appends the selection arguments --- @param string selection_name The name of the selection to listen for --- @param function handler The event handler -function Selection.on_selection(selection_name, handler) - Event.add(defines.events.on_player_selected_area, function(event) - local selection = selections[event.player_index] - if not selection or selection.name ~= selection_name then return end - handler(event, table.unpack(selection.arguments)) - end) -end - ---- Filter on_player_alt_selected_area to this custom selection, appends the selection arguments --- @param string selection_name The name of the selection to listen for --- @param function handler The event handler -function Selection.on_alt_selection(selection_name, handler) - Event.add(defines.events.on_player_alt_selected_area, function(event) - local selection = selections[event.player_index] - if not selection or selection.name ~= selection_name then return end - handler(event, table.unpack(selection.arguments)) - end) -end - ---- Stop selection if the selection tool is removed from the cursor -Event.add(defines.events.on_player_cursor_stack_changed, function(event) - local player = game.players[event.player_index] --- @cast player -nil - 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 -Event.add(defines.events.on_player_controller_changed, function(event) - local player = game.players[event.player_index] --- @cast player -nil - 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 such as death or leaving the game -local function stop_after_event(event) - local player = game.players[event.player_index] - Selection.stop(player) -end - -Event.add(defines.events.on_pre_player_left_game, stop_after_event) -Event.add(defines.events.on_pre_player_died, stop_after_event) - ---- Stop selection after a single use if single_use was true during Selection.start -local function stop_after_use(event) - if not selections[event.player_index] then return end - if not selections[event.player_index].single_use then return end - stop_after_event(event) -end - -Event.add(defines.events.on_player_selected_area, stop_after_use) -Event.add(defines.events.on_player_alt_selected_area, stop_after_use) - -return Selection diff --git a/exp_legacy/module/modules/gui/vlayer.lua b/exp_legacy/module/modules/gui/vlayer.lua index a84b5803..a57f090a 100644 --- a/exp_legacy/module/modules/gui/vlayer.lua +++ b/exp_legacy/module/modules/gui/vlayer.lua @@ -10,8 +10,9 @@ local Event = require("modules/exp_legacy/utils/event") --- @dep utils.event local format_number = require("util").format_number --- @dep util local config = require("modules.exp_legacy.config.vlayer") --- @dep config.vlayer local vlayer = require("modules.exp_legacy.modules.control.vlayer") -local Selection = require("modules.exp_legacy.modules.control.selection") --- @dep modules.control.selection -local SelectionConvertArea = "VlayerConvertChest" + +local Selection = require("modules/exp_util/selection") +local SelectArea = Selection.connect("ExpGui_VLayer") --- Align an aabb to the grid by expanding it local function aabb_align_expand(aabb) @@ -72,13 +73,15 @@ local function format_energy(amount, unit) return formatted .. " " .. suffix .. unit end +local ExpUtil = require("modules/exp_util") --- When an area is selected to add protection to the area -Selection.on_selection(SelectionConvertArea, function(event) +SelectArea:on_selection(function(event) + log(ExpUtil.format_any(event)) local area = aabb_align_expand(event.area) local player = game.players[event.player_index] if not player then - return nil + return end local container = Gui.get_left_element(vlayer_container, player) @@ -91,7 +94,7 @@ Selection.on_selection(SelectionConvertArea, function(event) entities = event.surface.find_entities_filtered{ area = area, name = "constant-combinator", force = player.force } else player.print{ "vlayer.power-on-space-research", config.power_on_space_research.name, config.power_on_space_research.level } - return nil + return end else entities = event.surface.find_entities_filtered{ area = area, name = "steel-chest", force = player.force } @@ -99,14 +102,14 @@ Selection.on_selection(SelectionConvertArea, function(event) if #entities == 0 then player.print{ "vlayer.steel-chest-detect" } - return nil + return elseif #entities > 1 then player.print{ "vlayer.result-unable", { "vlayer.control-type-" .. target:gsub("_", "-") }, { "vlayer.result-multiple" } } - return nil + return end if not entities[1] then - return nil + return end local e = entities[1] @@ -115,12 +118,12 @@ Selection.on_selection(SelectionConvertArea, function(event) if e.name and e.name == "steel-chest" and (not e.get_inventory(defines.inventory.chest).is_empty()) then player.print{ "vlayer.steel-chest-empty" } - return nil + return end if (vlayer.get_interface_counts()[target] >= config.interface_limit[target]) then player.print{ "vlayer.result-unable", { "vlayer.control-type-" .. target:gsub("_", "-") }, { "vlayer.result-limit" } } - return nil + return end e.destroy() @@ -128,7 +131,7 @@ Selection.on_selection(SelectionConvertArea, function(event) if target == "energy" then if not vlayer.create_energy_interface(event.surface, e_pos, player) then player.print{ "vlayer.result-unable", { "vlayer.control-type-energy" }, { "vlayer.result-space" } } - return nil + return end elseif target == "circuit" then vlayer.create_circuit_interface(event.surface, e_pos, e_circ, player) @@ -400,11 +403,10 @@ local vlayer_gui_control_build = Gui.define("vlayer_gui_control_build") }:style{ width = 200, }:on_click(function(def, player, element) - if Selection.is_selecting(player, SelectionConvertArea) then - Selection.stop(player) + if SelectArea:stop(player) then player.print{ "vlayer.exit" } else - Selection.start(player, SelectionConvertArea) + SelectArea:start(player) player.print{ "vlayer.enter" } end diff --git a/exp_scenario/module/commands/artillery.lua b/exp_scenario/module/commands/artillery.lua index 4d5ee260..95749da9 100644 --- a/exp_scenario/module/commands/artillery.lua +++ b/exp_scenario/module/commands/artillery.lua @@ -4,8 +4,9 @@ Adds a command that helps shoot artillery local AABB = require("modules/exp_util/aabb") local Commands = require("modules/exp_commands") -local Selection = require("modules.exp_legacy.modules.control.selection") --- @dep modules.control.selection -local SelectionName = "ExpCommand_Artillery" + +local Selection = require("modules/exp_util/selection") +local SelectArea = Selection.connect("ExpCommand_Artillery") local floor = math.floor local abs = math.abs @@ -37,18 +38,15 @@ end --- @overload fun(player: LuaPlayer) commands.artillery = Commands.new("artillery", { "exp-commands_artillery.description" }) :register(function(player) - if Selection.is_selecting(player, SelectionName) then - Selection.stop(player) + if SelectArea:stop(player) then return Commands.status.success{ "exp-commands_artillery.exit" } - else - Selection.start(player, SelectionName) - return Commands.status.success{ "exp-commands_artillery.enter" } end + SelectArea:start(player) + return Commands.status.success{ "exp-commands_artillery.enter" } end) --[[ @as any ]] --- when an area is selected to add protection to the area -Selection.on_selection(SelectionName, function(event) - --- @cast event EventData.on_player_selected_area +SelectArea:on_selection(function(event) local area = AABB.expand(event.area) local player = game.players[event.player_index] local surface = event.surface diff --git a/exp_scenario/module/commands/protected_entities.lua b/exp_scenario/module/commands/protected_entities.lua index fab57839..34426b24 100644 --- a/exp_scenario/module/commands/protected_entities.lua +++ b/exp_scenario/module/commands/protected_entities.lua @@ -12,14 +12,14 @@ local Commands = require("modules/exp_commands") local format_player_name = Commands.format_player_name_locale local Roles = require("modules.exp_legacy.expcore.roles") --- @dep expcore.roles -local Selection = require("modules.exp_legacy.modules.control.selection") --- @dep modules.control.selection local EntityProtection = require("modules.exp_legacy.modules.control.protection") --- @dep modules.control.protection local format_string = string.format local floor = math.floor -local SelectionNameEntity = "ExpCommand_ProtectEntity" -local SelectionNameArea = "ExpCommand_ProtectArea" +local Selection = require("modules/exp_util/selection") +local SelectEntities = Selection.connect("ExpCommand_ProtectEntity") +local SelectArea = Selection.connect("ExpCommand_ProtectArea") local renders = {} --- @type table> Stores all renders for a player Storage.register({ @@ -95,31 +95,26 @@ end Commands.new("protect-entity", { "exp-commands_entity-protection.description-entity" }) :add_aliases{ "pe" } :register(function(player) - if Selection.is_selecting(player, SelectionNameEntity) then - Selection.stop(player) + if SelectEntities:stop(player) then return Commands.status.success{ "exp-commands_entity-protection.exit-entity" } - else - Selection.start(player, SelectionNameEntity) - return Commands.status.success{ "exp-commands_entity-protection.enter-entity" } end + SelectEntities:start(player) + return Commands.status.success{ "exp-commands_entity-protection.enter-entity" } end) --- Toggles area protection selection Commands.new("protect-area", { "exp-commands_entity-protection.description-area" }) :add_aliases{ "pa" } :register(function(player) - if Selection.is_selecting(player, SelectionNameEntity) then - Selection.stop(player) + if SelectArea:stop(player) then return Commands.status.success{ "exp-commands_entity-protection.exit-area" } - else - Selection.start(player, SelectionNameEntity) - return Commands.status.success{ "exp-commands_entity-protection.enter-area" } end + SelectArea:start(player) + return Commands.status.success{ "exp-commands_entity-protection.enter-area" } end) --- When an area is selected to add protection to entities -Selection.on_selection(SelectionNameEntity, function(event) - --- @cast event EventData.on_player_selected_area +SelectEntities:on_selection(function(event) local player = game.players[event.player_index] for _, entity in ipairs(event.entities) do EntityProtection.add_entity(entity) @@ -130,8 +125,7 @@ Selection.on_selection(SelectionNameEntity, function(event) end) --- When an area is selected to remove protection from entities -Selection.on_alt_selection(SelectionNameEntity, function(event) - --- @cast event EventData.on_player_alt_selected_area +SelectEntities:on_alt_selection(function(event) local player = game.players[event.player_index] for _, entity in ipairs(event.entities) do EntityProtection.remove_entity(entity) @@ -142,8 +136,7 @@ Selection.on_alt_selection(SelectionNameEntity, function(event) end) --- When an area is selected to add protection to the area -Selection.on_selection(SelectionNameArea, function(event) - --- @cast event EventData.on_player_selected_area +SelectArea:on_selection(function(event) local surface = event.surface local area = expand_area(event.area) local areas = EntityProtection.get_areas(event.surface) @@ -160,8 +153,7 @@ Selection.on_selection(SelectionNameArea, function(event) end) --- When an area is selected to remove protection from the area -Selection.on_alt_selection(SelectionNameArea, function(event) - --- @cast event EventData.on_player_alt_selected_area +SelectArea:on_alt_selection(function(event) local surface = event.surface local area = expand_area(event.area) local areas = EntityProtection.get_areas(surface) @@ -177,7 +169,6 @@ end) --- When selection starts show all protected entities and protected areas local function on_player_selection_start(event) - if event.selection ~= SelectionNameEntity and event.selection ~= SelectionNameArea then return end local player = game.players[event.player_index] local surface = player.surface -- Allow remote view renders[player.index] = {} @@ -210,8 +201,7 @@ local function on_player_selection_start(event) end --- When selection ends hide protected entities and protected areas -local function on_player_selection_end(event) - if event.selection ~= SelectionNameEntity and event.selection ~= SelectionNameArea then return end +local function on_player_selection_stop(event) for _, render in pairs(renders[event.player_index]) do if render.valid then render.destroy() end end @@ -219,6 +209,11 @@ local function on_player_selection_end(event) renders[event.player_index] = nil end +SelectArea:on_start(on_player_selection_start) +SelectEntities:on_start(on_player_selection_start) +SelectArea:on_stop(on_player_selection_stop) +SelectEntities:on_stop(on_player_selection_stop) + --- When there is a repeat offence print it in chat local function on_repeat_violation(event) Roles.print_to_roles_higher("Regular", { @@ -232,8 +227,6 @@ end return { events = { - [Selection.events.on_player_selection_start] = on_player_selection_start, - [Selection.events.on_player_selection_end] = on_player_selection_end, [EntityProtection.events.on_repeat_violation] = on_repeat_violation, } } diff --git a/exp_scenario/module/commands/waterfill.lua b/exp_scenario/module/commands/waterfill.lua index 1958d37b..4ca95ec9 100644 --- a/exp_scenario/module/commands/waterfill.lua +++ b/exp_scenario/module/commands/waterfill.lua @@ -4,8 +4,9 @@ Adds a command that places shallow water local AABB = require("modules/exp_util/aabb") local Commands = require("modules/exp_commands") -local Selection = require("modules.exp_legacy.modules.control.selection") --- @dep modules.control.selection -local SelectionName = "ExpCommand_Waterfill" + +local Selection = require("modules/exp_util/selection") +local SelectArea = Selection.connect("ExpCommand_Waterfill") local planet = { ["nauvis"] = "water-mud", @@ -23,25 +24,22 @@ local commands = {} --- @overload fun(player: LuaPlayer) commands.waterfill = Commands.new("waterfill", { "exp-commands_waterfill.description" }) :register(function(player) - if Selection.is_selecting(player, SelectionName) then - Selection.stop(player) + if SelectArea:stop(player) then return Commands.status.success{ "exp-commands_waterfill.exit" } + end + local item_count_cliff = player.get_item_count("cliff-explosives") + local item_count_craft = math.min(math.floor(player.get_item_count("explosives") / 10), player.get_item_count("barrel"), player.get_item_count("grenade")) + local item_count_total = item_count_cliff + item_count_craft + if item_count_total == 0 then + return Commands.status.error{ "exp-commands_waterfill.requires-explosives" } else - local item_count_cliff = player.get_item_count("cliff-explosives") - local item_count_craft = math.min(math.floor(player.get_item_count("explosives") / 10), player.get_item_count("barrel"), player.get_item_count("grenade")) - local item_count_total = item_count_cliff + item_count_craft - if item_count_total == 0 then - return Commands.status.error{ "exp-commands_waterfill.requires-explosives" } - else - Selection.start(player, SelectionName) - return Commands.status.success{ "exp-commands_waterfill.enter" } - end + SelectArea:start(player) + return Commands.status.success{ "exp-commands_waterfill.enter" } end end) --[[ @as any ]] --- When an area is selected to be converted to water -Selection.on_selection(SelectionName, function(event) - --- @cast event EventData.on_player_selected_area +SelectArea:on_selection(function(event) local area = AABB.expand(event.area) local player = game.players[event.player_index] local surface = event.surface diff --git a/exp_scenario/module/gui/module_inserter.lua b/exp_scenario/module/gui/module_inserter.lua index c6116883..92cd5694 100644 --- a/exp_scenario/module/gui/module_inserter.lua +++ b/exp_scenario/module/gui/module_inserter.lua @@ -5,8 +5,9 @@ Adds a Gui which creates an selection planner to insert modules into buildings local Gui = require("modules/exp_gui") local AABB = require("modules/exp_util/aabb") local Roles = require("modules/exp_legacy/expcore/roles") -local Selection = require("modules/exp_legacy/modules/control/selection") -local SelectionModuleArea = "ModuleArea" + +local Selection = require("modules/exp_util/selection") +local SelectArea = Selection.connect("ModuleArea") local config = require("modules/exp_legacy/config/module") @@ -76,7 +77,7 @@ Elements.create_selection_planner = Gui.define("module_inserter/create_selection ) :on_click(function(def, player, element) --- @cast def ExpGui_ModuleInserter.elements.create_selection_planner - Selection.start(player, SelectionModuleArea, false, def.data[element]) + SelectArea:start(player, def.data[element]) end) --[[ @as any ]] --- Used to select the machine to apply modules to @@ -311,7 +312,7 @@ end --- When an area is selected to have module changes applied to it --- @param event EventData.on_player_selected_area --- @param module_table LuaGuiElement -Selection.on_selection(SelectionModuleArea, function(event, module_table) +SelectArea:on_selection(function(event, module_table) local player = Gui.get_player(event) local area = AABB.expand(event.area) diff --git a/exp_scenario/module/gui/research_milestones.lua b/exp_scenario/module/gui/research_milestones.lua index 4f6d9f19..42ca5f7e 100644 --- a/exp_scenario/module/gui/research_milestones.lua +++ b/exp_scenario/module/gui/research_milestones.lua @@ -301,8 +301,9 @@ end --- @param force LuaForce --- @return number function Elements.container.calculate_starting_research_index(force) - local force_data = Elements.container.data[force] + local force_data = Elements.container.data[force] or {} local research_index = research_targets.length + Elements.container.data[force] = force_data -- needed because of @clusterio/research_sync -- # does not work here because it returned the array alloc size for i = 1, research_targets.length do diff --git a/exp_util/module/module.json b/exp_util/module/module.json index 0e397b4a..0ea1ad10 100644 --- a/exp_util/module/module.json +++ b/exp_util/module/module.json @@ -4,6 +4,7 @@ "include/package.lua", "include/require.lua", "storage.lua", + "selection.lua", "async.lua" ], "require": [ diff --git a/exp_util/module/selection.lua b/exp_util/module/selection.lua new file mode 100644 index 00000000..a50a7940 --- /dev/null +++ b/exp_util/module/selection.lua @@ -0,0 +1,253 @@ +--[[-- 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 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 + _registered = {}, + + --- @package + events = {}, +} + +--- @class Selection +--- @field name string +--- @field _handlers table +Selection._prototype = {} + +Selection._metatable = { + __index = Selection._prototype, + __class = "Selection", +} + +--- @type table +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 fun(self: Selection, callback: fun(event: E, ...: any)): Selection + +--- @type Selection.on_event +Selection._prototype.on_start = on_event_factory(Selection.on_selection_start) + +--- @type Selection.on_event +Selection._prototype.on_stop = on_event_factory(Selection.on_selection_stop) + +--- @type Selection.on_event +Selection._prototype.on_selection = on_event_factory(e.on_player_selected_area) + +--- @type Selection.on_event +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