From 27a2feaae9986a14cb67d74bb0e5c6c5910c3c4e Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Thu, 4 Sep 2025 10:54:55 +0100 Subject: [PATCH 1/8] Refactor Selection Util (#409) * Add Selection to ExpUtil * Convert modules to use new lib * Bug Fixes --- .../module/modules/control/selection.lua | 194 -------------- exp_legacy/module/modules/gui/vlayer.lua | 30 ++- exp_scenario/module/commands/artillery.lua | 16 +- .../module/commands/protected_entities.lua | 45 ++-- exp_scenario/module/commands/waterfill.lua | 28 +- exp_scenario/module/gui/module_inserter.lua | 9 +- .../module/gui/research_milestones.lua | 3 +- exp_util/module/module.json | 1 + exp_util/module/selection.lua | 253 ++++++++++++++++++ 9 files changed, 316 insertions(+), 263 deletions(-) delete mode 100644 exp_legacy/module/modules/control/selection.lua create mode 100644 exp_util/module/selection.lua 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 From 5bd6183bb3264e300a4f8be39c47f521cc200cfd Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Thu, 4 Sep 2025 11:38:05 +0100 Subject: [PATCH 2/8] Fix desync of `online_player_dropdown._player_names` Fixes: #410 --- exp_scenario/module/gui/elements.lua | 55 +++++++++++++++++++--------- 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/exp_scenario/module/gui/elements.lua b/exp_scenario/module/gui/elements.lua index e1cd1113..66cfc99d 100644 --- a/exp_scenario/module/gui/elements.lua +++ b/exp_scenario/module/gui/elements.lua @@ -7,34 +7,57 @@ local Gui = require("modules/exp_gui") --- @class ExpGui_Elements local Elements = {} ---- To help with caching and avoid context changes the player list from the previous update is remembered ---- @type (string?)[] -local _player_names = {} - --- Dropdown which allows selecting an online player --- @class ExpGui_Elements.online_player_dropdown: ExpElement --- @overload fun(parent: LuaGuiElement): LuaGuiElement Elements.online_player_dropdown = Gui.define("player_dropdown") :track_all_elements() :draw(function(def, parent) + local player_names = Elements.online_player_dropdown._access_player_names() return parent.add{ type = "drop-down", - items = _player_names, - selected_index = #_player_names > 0 and 1 or nil, + items = player_names, + selected_index = 1, } end) :style{ height = 24, } --[[ @as any ]] +--- To help with caching and avoid context changes the player list from the previous update is remembered +--- @type (string?)[] +do local _player_names = {} + --- Updates the player name list after a join or leave + --- @return (string?)[] + function Elements.online_player_dropdown._update_player_names() + _player_names[#_player_names] = nil -- Nil last element to account for player leave + for i, player in pairs(game.connected_players) do + _player_names[i] = player.name + end + return _player_names + end + + --- Safely access the player name list + --- @return (string?)[] + function Elements.online_player_dropdown._access_player_names() + if not _player_names[1] then + for i, player in pairs(game.connected_players) do + _player_names[i] = player.name + end + end + return _player_names + end +end + --- Get the selected player name from a online player dropdown --- @param online_player_dropdown LuaGuiElement --- @return string function Elements.online_player_dropdown.get_selected_name(online_player_dropdown) - local name = _player_names[online_player_dropdown.selected_index] + local player_names = Elements.online_player_dropdown._access_player_names() + local name = player_names[online_player_dropdown.selected_index] if not name then online_player_dropdown.selected_index = 1 - name = _player_names[1] --- @cast name -nil + name = player_names[1] --- @cast name -nil end return name end @@ -43,10 +66,11 @@ end --- @param online_player_dropdown LuaGuiElement --- @return LuaPlayer function Elements.online_player_dropdown.get_selected(online_player_dropdown) - local name = _player_names[online_player_dropdown.selected_index] + local player_names = Elements.online_player_dropdown._access_player_names() + local name = player_names[online_player_dropdown.selected_index] if not name then online_player_dropdown.selected_index = 1 - name = _player_names[1] --- @cast name -nil + name = player_names[1] --- @cast name -nil end return assert(game.get_player(name)) end @@ -55,20 +79,15 @@ end --- Get the number of players in the dropdown --- @return number function Elements.online_player_dropdown.get_player_count() - return #_player_names + return #Elements.online_player_dropdown._access_player_names() end --- Update all player dropdowns to match the currently online players --- We don't split join and leave because the order would be inconsistent between players and cause desyncs function Elements.online_player_dropdown.refresh_online() - _player_names[#_player_names] = nil -- Nil last element to account for player leave - - for i, player in pairs(game.connected_players) do - _player_names[i] = player.name - end - + local player_names = Elements.online_player_dropdown._update_player_names() for _, online_player_dropdown in Elements.online_player_dropdown:online_elements() do - online_player_dropdown.items = _player_names + online_player_dropdown.items = player_names end end From d654005dae5385bfd65e76d846e06bf4be04abd6 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Thu, 4 Sep 2025 11:58:14 +0100 Subject: [PATCH 3/8] Fix server ups active on first start --- exp_server_ups/instance.ts | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/exp_server_ups/instance.ts b/exp_server_ups/instance.ts index b0a97830..1783acc3 100644 --- a/exp_server_ups/instance.ts +++ b/exp_server_ups/instance.ts @@ -6,29 +6,42 @@ export class InstancePlugin extends BaseInstancePlugin { private gameTimes: number[] = []; async onStart() { - this.updateInterval = setInterval(this.updateUps.bind(this), this.instance.config.get("exp_server_ups.update_interval")); + if (!this.instance.config.get("factorio.settings")["auto_pause"]) { + this.setInterval(); + } } onExit() { - if (this.updateInterval) { - clearInterval(this.updateInterval); - } + this.clearInterval(); } async onInstanceConfigFieldChanged(field: string, curr: unknown): Promise { if (field === "exp_server_ups.update_interval") { - this.onExit(); - await this.onStart(); + this.clearInterval(); + this.setInterval(); } else if (field === "exp_server_ups.average_interval") { this.gameTimes.splice(curr as number); } } async onPlayerEvent(event: lib.PlayerEvent): Promise { - if (event.type === "join" && !this.updateInterval) { - await this.onStart(); - } else if (event.type === "leave" && this.instance.playersOnline.size == 0 && this.instance.config.get("factorio.settings")["auto_pause"] as boolean) { - this.onExit(); + if (event.type === "join") { + this.setInterval(); + } else if (event.type === "leave" && this.instance.playersOnline.size == 0 && this.instance.config.get("factorio.settings")["auto_pause"]) { + this.clearInterval(); + } + } + + setInterval() { + if (!this.updateInterval) { + this.updateInterval = setInterval(this.updateUps.bind(this), this.instance.config.get("exp_server_ups.update_interval")); + } + } + + clearInterval() { + if (this.updateInterval) { + clearInterval(this.updateInterval); + this.updateInterval = undefined; } } From 06d98458d46f30b495e27021ed9fa5ab757df216 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Thu, 4 Sep 2025 14:01:44 +0100 Subject: [PATCH 4/8] Fix module table row data not being cleared Fixes: #411 --- exp_scenario/module/gui/module_inserter.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exp_scenario/module/gui/module_inserter.lua b/exp_scenario/module/gui/module_inserter.lua index 92cd5694..9cb2bfd4 100644 --- a/exp_scenario/module/gui/module_inserter.lua +++ b/exp_scenario/module/gui/module_inserter.lua @@ -178,7 +178,7 @@ end function Elements.module_table.remove_row(module_table, machine_selector) local rows = Elements.module_table.data[module_table] local row = rows[machine_selector.index] - row[machine_selector.index] = nil + rows[machine_selector.index] = nil Gui.destroy_if_valid(machine_selector) for _, separator in pairs(row.row_separators) do Gui.destroy_if_valid(separator) From f1f4117e0f4c9ce30339320d9a527d0584f4a170 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Fri, 5 Sep 2025 12:45:55 +0100 Subject: [PATCH 5/8] Fix blueprint revive and missing script raise event (#401) Fixes: #397 --- exp_legacy/module/modules/gui/rocket-info.lua | 4 +++- exp_scenario/module/commands/repair.lua | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/exp_legacy/module/modules/gui/rocket-info.lua b/exp_legacy/module/modules/gui/rocket-info.lua index d5af1e51..e31efea9 100644 --- a/exp_legacy/module/modules/gui/rocket-info.lua +++ b/exp_legacy/module/modules/gui/rocket-info.lua @@ -549,7 +549,7 @@ Event.on_nth_tick(150, function() end) --- Adds a silo to the list when it is built ---- @param event EventData.on_built_entity | EventData.on_robot_built_entity +--- @param event EventData.on_built_entity | EventData.on_robot_built_entity | EventData.script_raised_built | EventData.script_raised_revive local function on_built(event) local entity = event.entity if entity.valid and entity.name == "rocket-silo" then @@ -559,6 +559,8 @@ end Event.add(defines.events.on_built_entity, on_built) Event.add(defines.events.on_robot_built_entity, on_built) +Event.add(defines.events.script_raised_built, on_built) +Event.add(defines.events.script_raised_revive, on_built) --- Redraw the progress section on role change local function role_update_event(event) diff --git a/exp_scenario/module/commands/repair.lua b/exp_scenario/module/commands/repair.lua index 019c1dee..b52a4cfc 100644 --- a/exp_scenario/module/commands/repair.lua +++ b/exp_scenario/module/commands/repair.lua @@ -24,11 +24,11 @@ Commands.new("repair", { "exp-commands_repair.description" }) force = force, } + local param = { raise_revive = true } --- @type LuaEntity.silent_revive_param for _, entity in ipairs(entities) do - -- TODO test for ghost not being a blueprint, https://forums.factorio.com/viewtopic.php?f=28&t=119736 - if not config.disallow[entity.ghost_name] and (config.allow_blueprint_repair or true) then + if not config.disallow[entity.ghost_name] and (config.allow_blueprint_repair or entity.created_by_corpse) then revive_count = revive_count + 1 - entity.silent_revive() + entity.silent_revive(param) end end From 6bb26f60adc568709920721afb6e14ca4eb247ee Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Fri, 12 Sep 2025 20:47:32 +0100 Subject: [PATCH 6/8] Add webpack to all plugins --- exp_commands/package.json | 7 ++++-- exp_commands/tsconfig.browser.json | 4 ++++ exp_commands/tsconfig.json | 3 ++- exp_commands/web/index.tsx | 0 exp_commands/webpack.config.js | 29 ++++++++++++++++++++++++ exp_gui/package.json | 7 ++++-- exp_gui/tsconfig.browser.json | 4 ++++ exp_gui/tsconfig.json | 3 ++- exp_gui/web/index.tsx | 0 exp_gui/webpack.config.js | 29 ++++++++++++++++++++++++ exp_legacy/package.json | 7 ++++-- exp_legacy/tsconfig.browser.json | 4 ++++ exp_legacy/tsconfig.json | 3 ++- exp_legacy/web/index.tsx | 0 exp_legacy/webpack.config.js | 29 ++++++++++++++++++++++++ exp_server_ups/tsconfig.json | 3 ++- exp_util/package.json | 7 ++++-- exp_util/tsconfig.browser.json | 4 ++++ exp_util/tsconfig.json | 3 ++- exp_util/web/index.tsx | 0 exp_util/webpack.config.js | 29 ++++++++++++++++++++++++ pnpm-lock.yaml | 36 ++++++++++++++++++++++++++++++ 22 files changed, 198 insertions(+), 13 deletions(-) create mode 100644 exp_commands/tsconfig.browser.json create mode 100644 exp_commands/web/index.tsx create mode 100644 exp_commands/webpack.config.js create mode 100644 exp_gui/tsconfig.browser.json create mode 100644 exp_gui/web/index.tsx create mode 100644 exp_gui/webpack.config.js create mode 100644 exp_legacy/tsconfig.browser.json create mode 100644 exp_legacy/web/index.tsx create mode 100644 exp_legacy/webpack.config.js create mode 100644 exp_util/tsconfig.browser.json create mode 100644 exp_util/web/index.tsx create mode 100644 exp_util/webpack.config.js diff --git a/exp_commands/package.json b/exp_commands/package.json index 1d8f7749..1bcf6c28 100644 --- a/exp_commands/package.json +++ b/exp_commands/package.json @@ -7,7 +7,7 @@ "repository": "explosivegaming/ExpCluster", "main": "dist/node/index.js", "scripts": { - "prepare": "tsc --build" + "prepare": "tsc --build && webpack-cli --env production" }, "engines": { "node": ">=18" @@ -18,7 +18,10 @@ "devDependencies": { "@clusterio/lib": "catalog:", "@types/node": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "webpack": "catalog:", + "webpack-cli": "catalog:", + "webpack-merge": "catalog:" }, "dependencies": { "@expcluster/lib_util": "workspace:^", diff --git a/exp_commands/tsconfig.browser.json b/exp_commands/tsconfig.browser.json new file mode 100644 index 00000000..1ab2704e --- /dev/null +++ b/exp_commands/tsconfig.browser.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.browser.json", + "include": [ "web/**/*.tsx", "web/**/*.ts", "package.json" ], +} diff --git a/exp_commands/tsconfig.json b/exp_commands/tsconfig.json index a0473e75..303b8f28 100644 --- a/exp_commands/tsconfig.json +++ b/exp_commands/tsconfig.json @@ -1,6 +1,7 @@ { "files": [], "references": [ - { "path": "./tsconfig.node.json" } + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.browser.json" } ] } diff --git a/exp_commands/web/index.tsx b/exp_commands/web/index.tsx new file mode 100644 index 00000000..e69de29b diff --git a/exp_commands/webpack.config.js b/exp_commands/webpack.config.js new file mode 100644 index 00000000..88d7cdf9 --- /dev/null +++ b/exp_commands/webpack.config.js @@ -0,0 +1,29 @@ +"use strict"; +const path = require("path"); +const webpack = require("webpack"); +const { merge } = require("webpack-merge"); + +const common = require("@clusterio/web_ui/webpack.common"); + +module.exports = (env = {}) => merge(common(env), { + context: __dirname, + entry: "./web/index.tsx", + output: { + path: path.resolve(__dirname, "dist", "web"), + }, + plugins: [ + new webpack.container.ModuleFederationPlugin({ + name: "exp_commands", + library: { type: "window", name: "plugin_exp_commands" }, + exposes: { + "./": "./index.ts", + "./package.json": "./package.json", + "./web": "./web/index.tsx", + }, + shared: { + "@clusterio/lib": { import: false }, + "@clusterio/web_ui": { import: false }, + }, + }), + ], +}); diff --git a/exp_gui/package.json b/exp_gui/package.json index 8e423254..4e112edb 100644 --- a/exp_gui/package.json +++ b/exp_gui/package.json @@ -7,7 +7,7 @@ "repository": "explosivegaming/ExpCluster", "main": "dist/node/index.js", "scripts": { - "prepare": "tsc --build" + "prepare": "tsc --build && webpack-cli --env production" }, "engines": { "node": ">=18" @@ -18,7 +18,10 @@ "devDependencies": { "@clusterio/lib": "catalog:", "@types/node": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "webpack": "catalog:", + "webpack-cli": "catalog:", + "webpack-merge": "catalog:" }, "dependencies": { "@expcluster/lib_util": "workspace:^", diff --git a/exp_gui/tsconfig.browser.json b/exp_gui/tsconfig.browser.json new file mode 100644 index 00000000..1ab2704e --- /dev/null +++ b/exp_gui/tsconfig.browser.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.browser.json", + "include": [ "web/**/*.tsx", "web/**/*.ts", "package.json" ], +} diff --git a/exp_gui/tsconfig.json b/exp_gui/tsconfig.json index a0473e75..303b8f28 100644 --- a/exp_gui/tsconfig.json +++ b/exp_gui/tsconfig.json @@ -1,6 +1,7 @@ { "files": [], "references": [ - { "path": "./tsconfig.node.json" } + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.browser.json" } ] } diff --git a/exp_gui/web/index.tsx b/exp_gui/web/index.tsx new file mode 100644 index 00000000..e69de29b diff --git a/exp_gui/webpack.config.js b/exp_gui/webpack.config.js new file mode 100644 index 00000000..640e5770 --- /dev/null +++ b/exp_gui/webpack.config.js @@ -0,0 +1,29 @@ +"use strict"; +const path = require("path"); +const webpack = require("webpack"); +const { merge } = require("webpack-merge"); + +const common = require("@clusterio/web_ui/webpack.common"); + +module.exports = (env = {}) => merge(common(env), { + context: __dirname, + entry: "./web/index.tsx", + output: { + path: path.resolve(__dirname, "dist", "web"), + }, + plugins: [ + new webpack.container.ModuleFederationPlugin({ + name: "exp_gui", + library: { type: "window", name: "plugin_exp_gui" }, + exposes: { + "./": "./index.ts", + "./package.json": "./package.json", + "./web": "./web/index.tsx", + }, + shared: { + "@clusterio/lib": { import: false }, + "@clusterio/web_ui": { import: false }, + }, + }), + ], +}); diff --git a/exp_legacy/package.json b/exp_legacy/package.json index 2c1d50c7..5ab1fc1f 100644 --- a/exp_legacy/package.json +++ b/exp_legacy/package.json @@ -7,7 +7,7 @@ "repository": "explosivegaming/ExpCluster", "main": "dist/node/index.js", "scripts": { - "prepare": "tsc --build" + "prepare": "tsc --build && webpack-cli --env production" }, "engines": { "node": ">=18" @@ -18,7 +18,10 @@ "devDependencies": { "@clusterio/lib": "catalog:", "@types/node": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "webpack": "catalog:", + "webpack-cli": "catalog:", + "webpack-merge": "catalog:" }, "dependencies": { "@expcluster/lib_commands": "workspace:^", diff --git a/exp_legacy/tsconfig.browser.json b/exp_legacy/tsconfig.browser.json new file mode 100644 index 00000000..1ab2704e --- /dev/null +++ b/exp_legacy/tsconfig.browser.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.browser.json", + "include": [ "web/**/*.tsx", "web/**/*.ts", "package.json" ], +} diff --git a/exp_legacy/tsconfig.json b/exp_legacy/tsconfig.json index a0473e75..303b8f28 100644 --- a/exp_legacy/tsconfig.json +++ b/exp_legacy/tsconfig.json @@ -1,6 +1,7 @@ { "files": [], "references": [ - { "path": "./tsconfig.node.json" } + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.browser.json" } ] } diff --git a/exp_legacy/web/index.tsx b/exp_legacy/web/index.tsx new file mode 100644 index 00000000..e69de29b diff --git a/exp_legacy/webpack.config.js b/exp_legacy/webpack.config.js new file mode 100644 index 00000000..c5405881 --- /dev/null +++ b/exp_legacy/webpack.config.js @@ -0,0 +1,29 @@ +"use strict"; +const path = require("path"); +const webpack = require("webpack"); +const { merge } = require("webpack-merge"); + +const common = require("@clusterio/web_ui/webpack.common"); + +module.exports = (env = {}) => merge(common(env), { + context: __dirname, + entry: "./web/index.tsx", + output: { + path: path.resolve(__dirname, "dist", "web"), + }, + plugins: [ + new webpack.container.ModuleFederationPlugin({ + name: "exp_legacy", + library: { type: "window", name: "plugin_exp_legacy" }, + exposes: { + "./": "./index.ts", + "./package.json": "./package.json", + "./web": "./web/index.tsx", + }, + shared: { + "@clusterio/lib": { import: false }, + "@clusterio/web_ui": { import: false }, + }, + }), + ], +}); diff --git a/exp_server_ups/tsconfig.json b/exp_server_ups/tsconfig.json index a0473e75..303b8f28 100644 --- a/exp_server_ups/tsconfig.json +++ b/exp_server_ups/tsconfig.json @@ -1,6 +1,7 @@ { "files": [], "references": [ - { "path": "./tsconfig.node.json" } + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.browser.json" } ] } diff --git a/exp_util/package.json b/exp_util/package.json index 88b0af84..4ca76775 100644 --- a/exp_util/package.json +++ b/exp_util/package.json @@ -7,7 +7,7 @@ "repository": "explosivegaming/ExpCluster", "main": "dist/node/index.js", "scripts": { - "prepare": "tsc --build" + "prepare": "tsc --build && webpack-cli --env production" }, "engines": { "node": ">=18" @@ -18,7 +18,10 @@ "devDependencies": { "@clusterio/lib": "catalog:", "@types/node": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "webpack": "catalog:", + "webpack-cli": "catalog:", + "webpack-merge": "catalog:" }, "dependencies": { "@sinclair/typebox": "catalog:" diff --git a/exp_util/tsconfig.browser.json b/exp_util/tsconfig.browser.json new file mode 100644 index 00000000..1ab2704e --- /dev/null +++ b/exp_util/tsconfig.browser.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.browser.json", + "include": [ "web/**/*.tsx", "web/**/*.ts", "package.json" ], +} diff --git a/exp_util/tsconfig.json b/exp_util/tsconfig.json index a0473e75..303b8f28 100644 --- a/exp_util/tsconfig.json +++ b/exp_util/tsconfig.json @@ -1,6 +1,7 @@ { "files": [], "references": [ - { "path": "./tsconfig.node.json" } + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.browser.json" } ] } diff --git a/exp_util/web/index.tsx b/exp_util/web/index.tsx new file mode 100644 index 00000000..e69de29b diff --git a/exp_util/webpack.config.js b/exp_util/webpack.config.js new file mode 100644 index 00000000..fc72ab41 --- /dev/null +++ b/exp_util/webpack.config.js @@ -0,0 +1,29 @@ +"use strict"; +const path = require("path"); +const webpack = require("webpack"); +const { merge } = require("webpack-merge"); + +const common = require("@clusterio/web_ui/webpack.common"); + +module.exports = (env = {}) => merge(common(env), { + context: __dirname, + entry: "./web/index.tsx", + output: { + path: path.resolve(__dirname, "dist", "web"), + }, + plugins: [ + new webpack.container.ModuleFederationPlugin({ + name: "exp_util", + library: { type: "window", name: "plugin_exp_util" }, + exposes: { + "./": "./index.ts", + "./package.json": "./package.json", + "./web": "./web/index.tsx", + }, + shared: { + "@clusterio/lib": { import: false }, + "@clusterio/web_ui": { import: false }, + }, + }), + ], +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 86dbefca..58f4c8db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,6 +69,15 @@ importers: typescript: specifier: 'catalog:' version: 5.8.3 + webpack: + specifier: 'catalog:' + version: 5.98.0(@swc/core@1.11.16)(webpack-cli@5.1.4) + webpack-cli: + specifier: 'catalog:' + version: 5.1.4(webpack@5.98.0) + webpack-merge: + specifier: 'catalog:' + version: 5.10.0 exp_groups: dependencies: @@ -134,6 +143,15 @@ importers: typescript: specifier: 'catalog:' version: 5.8.3 + webpack: + specifier: 'catalog:' + version: 5.98.0(@swc/core@1.11.16)(webpack-cli@5.1.4) + webpack-cli: + specifier: 'catalog:' + version: 5.1.4(webpack@5.98.0) + webpack-merge: + specifier: 'catalog:' + version: 5.10.0 exp_legacy: dependencies: @@ -156,6 +174,15 @@ importers: typescript: specifier: 'catalog:' version: 5.8.3 + webpack: + specifier: 'catalog:' + version: 5.98.0(@swc/core@1.11.16)(webpack-cli@5.1.4) + webpack-cli: + specifier: 'catalog:' + version: 5.1.4(webpack@5.98.0) + webpack-merge: + specifier: 'catalog:' + version: 5.10.0 exp_scenario: dependencies: @@ -252,6 +279,15 @@ importers: typescript: specifier: 'catalog:' version: 5.8.3 + webpack: + specifier: 'catalog:' + version: 5.98.0(@swc/core@1.11.16)(webpack-cli@5.1.4) + webpack-cli: + specifier: 'catalog:' + version: 5.1.4(webpack@5.98.0) + webpack-merge: + specifier: 'catalog:' + version: 5.10.0 packages: From 478de7e42d7d91b7846d74495a803ec985b2e5dd Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Tue, 16 Sep 2025 21:08:12 +0100 Subject: [PATCH 7/8] Fix no player character during bonus reset --- exp_scenario/module/gui/player_bonus.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/exp_scenario/module/gui/player_bonus.lua b/exp_scenario/module/gui/player_bonus.lua index b136495f..34abc22e 100644 --- a/exp_scenario/module/gui/player_bonus.lua +++ b/exp_scenario/module/gui/player_bonus.lua @@ -401,6 +401,9 @@ end --- @param player LuaPlayer function Elements.container.clear_player_bonus(player) Elements.container.data[player] = {} + if not player.character then + return + end for _, bonus_data in pairs(config.player_bonus) do if not bonus_data.is_special then player[bonus_data.name] = 0 From 360247d292e431bb954caf17bc6706f9e12625b2 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Sun, 21 Sep 2025 19:45:42 +0100 Subject: [PATCH 8/8] Fix surface bonus application --- exp_scenario/module/control/bonus.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/exp_scenario/module/control/bonus.lua b/exp_scenario/module/control/bonus.lua index 7af386cd..e253b0f6 100644 --- a/exp_scenario/module/control/bonus.lua +++ b/exp_scenario/module/control/bonus.lua @@ -9,15 +9,16 @@ 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 - event.force[k] = v.initial_value + force[k] = v.initial_value 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.force_bonus) do + for k, v in pairs(config.surface_bonus) do surface[k] = v.initial_value end end