diff --git a/features/redmew_qol.lua b/features/redmew_qol.lua index 25a2a7308..e5309adad 100644 --- a/features/redmew_qol.lua +++ b/features/redmew_qol.lua @@ -34,9 +34,14 @@ local function pick_name() end local loader_frame_name = Gui.uid_name() +local loader_machine_frame_name = Gui.uid_name() local loader_button_player = Gui.uid_name() local loader_button_machine = Gui.uid_name() +-- Exposed for tests +Public._loader_frame_name = loader_frame_name +Public._loader_machine_frame_name = loader_machine_frame_name + local loaders = { ['loader'] = true, ['fast-loader'] = true, @@ -92,10 +97,11 @@ local function any_loader_enabled(recipes) end local function draw_loader_frame(parent, entity) - local frame = parent[loader_frame_name] + local frame_name = entity and loader_machine_frame_name or loader_frame_name + local frame = parent[frame_name] local player = entity or safe_get_player(parent.player_index) - local recipes = player.force.recipes - if not player or not any_loader_enabled(recipes) then + local recipes = player and player.force.recipes + if not recipes or not any_loader_enabled(recipes) then if frame and frame.valid then Gui.destroy(frame) end @@ -107,7 +113,7 @@ local function draw_loader_frame(parent, entity) else frame = parent.add { type = 'frame', - name = loader_frame_name, + name = frame_name, anchor = { gui = defines.relative_gui_type[entity and 'assembling_machine_select_recipe_gui' or 'controller_gui'], position = defines.relative_gui_position.right @@ -321,6 +327,9 @@ local features = { [defines.events.on_research_finished] = 'on_research_finished', [defines.events.on_gui_opened] = 'on_gui_opened', [defines.events.on_gui_closed] = 'on_gui_closed', + [defines.events.on_player_created] = 'on_player_created', + [defines.events.on_player_joined_game] = 'on_player_created', + [defines.events.on_player_changed_force] = 'on_player_created', }, handlers = { on_built = Token.register(snap_loader), @@ -330,10 +339,18 @@ local features = { return end e.research.force.recipes[recipe].enabled = true - for _, p in pairs(game.players) do - if p.opened_gui_type == defines.gui_type.controller then - draw_loader_frame(p.gui.relative) - end + for _, p in pairs(e.research.force.players) do + draw_loader_frame(p.gui.relative) + end + end), + -- The frame anchored to the controller GUI is kept in place permanently. + -- Creating it inside on_gui_opened instead would delay its appearance in + -- multiplayer by the latency round trip, as script events do not run in + -- the client's latency-hidden state. + on_player_created = Token.register(function(e) + local p = safe_get_player(e.player_index) + if p then + draw_loader_frame(p.gui.relative) end end), on_gui_opened = Token.register(function(e) @@ -353,12 +370,27 @@ local features = { if not p then return end - local frame = p.gui.relative[loader_frame_name] + local frame = p.gui.relative[loader_machine_frame_name] if frame and frame.valid then Gui.destroy(frame) end end) - } + }, + on_register = function() + for _, p in pairs(game.players) do + draw_loader_frame(p.gui.relative) + end + end, + on_unregister = function() + for _, p in pairs(game.players) do + for _, name in pairs({ loader_frame_name, loader_machine_frame_name }) do + local frame = p.gui.relative[name] + if frame and frame.valid then + Gui.destroy(frame) + end + end + end + end, }, { name = 'save_bots', @@ -379,6 +411,9 @@ local function register_feature(feature) for event_id, handler_id in pairs(feature.events) do Event.add_removable(event_id, feature.handlers[handler_id]) end + if feature.on_register and game then + feature.on_register() + end return true end @@ -387,6 +422,9 @@ local function unregister_feature(feature) Event.remove_removable(event_id, feature.handlers[handler_id]) end enabled[feature.name] = false + if feature.on_unregister and game then + feature.on_unregister() + end return true end @@ -409,7 +447,8 @@ end Gui.on_click(loader_button_player, function(event) local player = event.player local recipe = event.element.elem_value - if not player.force.recipes[recipe].enabled then + local force_recipe = recipe and player.force.recipes[recipe] + if not (force_recipe and force_recipe.enabled) then return end local count = (event.button == defines.mouse_button_type.left) and (event.shift and 4294967295 or 1) or (event.button == defines.mouse_button_type.right) and 5 or nil @@ -420,7 +459,8 @@ end) Gui.on_click(loader_button_machine, function(event) local recipe = event.element.elem_value - if not event.player.force.recipes[recipe].enabled then + local force_recipe = recipe and event.player.force.recipes[recipe] + if not (force_recipe and force_recipe.enabled) then return end local entity = Gui.get_data(event.element) @@ -439,6 +479,11 @@ local loader_check_token = Token.register(function() end end end + if enabled.loaders then + for _, p in pairs(game.players) do + draw_loader_frame(p.gui.relative) + end + end end) Event.on_init(function() @@ -452,9 +497,11 @@ Event.on_configuration_changed(function() Task.set_timeout_in_ticks(1, loader_check_token) end for _, p in pairs(game.players) do - local frame = p.gui.relative[loader_frame_name] - if frame then - Gui.destroy(frame) + for _, name in pairs({ loader_frame_name, loader_machine_frame_name }) do + local frame = p.gui.relative[name] + if frame then + Gui.destroy(frame) + end end end end) diff --git a/features/redmew_qol_tests.lua b/features/redmew_qol_tests.lua new file mode 100644 index 000000000..499957b75 --- /dev/null +++ b/features/redmew_qol_tests.lua @@ -0,0 +1,156 @@ +local Declare = require 'utils.test.declare' +local EventFactory = require 'utils.test.event_factory' +local Assert = require 'utils.test.assert' +local RedmewQol = require 'features.redmew_qol' + +local loader_recipes = { 'loader', 'fast-loader', 'express-loader', 'turbo-loader' } + +local function controller_frame(player) + return player.gui.relative[RedmewQol._loader_frame_name] +end + +local function machine_frame(player) + return player.gui.relative[RedmewQol._loader_machine_frame_name] +end + +local function raise_controller_gui_event(event_name, player) + EventFactory.raise({ + name = event_name, + tick = game.tick, + player_index = player.index, + gui_type = defines.gui_type.controller + }) +end + +local function count_loader_buttons(element) + local count = 0 + for _, child in pairs(element.children) do + if child.type == 'choose-elem-button' then + count = count + 1 + else + count = count + count_loader_buttons(child) + end + end + return count +end + +Declare.module({'features', 'redmew qol', 'loaders gui'}, function() + local old_feature_enabled + local old_recipe_enabled + + Declare.module_startup(function(context) + local force = context.player.force + old_feature_enabled = RedmewQol.get_loaders() + old_recipe_enabled = force.recipes['loader'].enabled + + force.recipes['loader'].enabled = true + if not old_feature_enabled then + RedmewQol.set_loaders(true) + end + end) + + Declare.module_teardown(function(context) + context.player.force.recipes['loader'].enabled = old_recipe_enabled + if not old_feature_enabled then + RedmewQol.set_loaders(false) + else + -- redraw with the restored recipe state + raise_controller_gui_event(defines.events.on_gui_opened, context.player) + end + end) + + Declare.test('frame is drawn for all players when the feature is enabled', function(context) + local player = context.player + + RedmewQol.set_loaders(false) + Assert.is_nil(controller_frame(player), 'frame should be removed when the feature is disabled') + + RedmewQol.set_loaders(true) + Assert.valid(controller_frame(player), 'frame should be drawn when the feature is enabled') + end) + + Declare.test('frame persists when the crafting menu is closed', function(context) + local player = context.player + + raise_controller_gui_event(defines.events.on_gui_opened, player) + Assert.valid(controller_frame(player), 'frame should exist after opening the crafting menu') + + raise_controller_gui_event(defines.events.on_gui_closed, player) + Assert.valid(controller_frame(player), 'frame should persist after closing the crafting menu') + end) + + Declare.test('frame is removed when no loader recipe is enabled', function(context) + local player = context.player + local recipes = player.force.recipes + + local old_states = {} + for _, name in pairs(loader_recipes) do + local recipe = recipes[name] + if recipe then + old_states[name] = recipe.enabled + recipe.enabled = false + end + end + + raise_controller_gui_event(defines.events.on_gui_opened, player) + local removed = controller_frame(player) == nil + + for name, state in pairs(old_states) do + recipes[name].enabled = state + end + raise_controller_gui_event(defines.events.on_gui_opened, player) + + Assert.is_true(removed, 'frame should be removed when no loader recipe is enabled') + Assert.valid(controller_frame(player), 'frame should be drawn again when a loader recipe is enabled') + end) + + Declare.test('frame lists one button per enabled loader recipe', function(context) + local player = context.player + + raise_controller_gui_event(defines.events.on_gui_opened, player) + + local expected = 0 + for _, name in pairs(loader_recipes) do + local recipe = player.force.recipes[name] + if recipe and recipe.enabled then + expected = expected + 1 + end + end + + Assert.equal(expected, count_loader_buttons(controller_frame(player)), 'frame should have one button per enabled loader recipe') + end) + + Declare.test('machine frame is separate and removed when its gui is closed', function(context) + local player = context.player + local surface = player.surface + local position = surface.find_non_colliding_position('assembling-machine-1', player.position, 32, 1) + local machine = surface.create_entity({ name = 'assembling-machine-1', position = position, force = player.force }) + + raise_controller_gui_event(defines.events.on_gui_opened, player) + + EventFactory.raise({ + name = defines.events.on_gui_opened, + tick = game.tick, + player_index = player.index, + gui_type = defines.gui_type.entity, + entity = machine + }) + local machine_frame_drawn = machine_frame(player) ~= nil + + EventFactory.raise({ + name = defines.events.on_gui_closed, + tick = game.tick, + player_index = player.index, + gui_type = defines.gui_type.entity, + entity = machine + }) + local machine_frame_removed = machine_frame(player) == nil + local controller_frame_kept = controller_frame(player) ~= nil + + machine.destroy() + + Assert.is_true(machine_frame_drawn, 'machine frame should be drawn when a machine gui is opened') + Assert.is_true(machine_frame_removed, 'machine frame should be removed when the machine gui is closed') + Assert.is_true(controller_frame_kept, 'controller frame should not be affected by the machine gui closing') + end) +end)