diff --git a/control.lua b/control.lua index 25bc8a8..3874681 100644 --- a/control.lua +++ b/control.lua @@ -30,3 +30,5 @@ require "src.auto_player_request" require "src.cheat_production_supply" require "src.remote_interface" + +require "src.my_entity_gui" diff --git a/info.json b/info.json index ab421b4..5e8f07e 100644 --- a/info.json +++ b/info.json @@ -1,9 +1,9 @@ { - "name": "item-network", + "name": "cheat-network", "version": "1.7.3", - "title": "Item Network (BNG)", - "author": "year6b7a", + "title": "Cheat Network", + "author": "Dreaden", "factorio_version": "1.1", "dependencies": ["base >= 1.1", "stdlib >= 1.4.8"], - "description": "This mod adds a network for items and fluids." + "description": "This mod adds a cheat network for items and fluids." } diff --git a/src/EntityService.lua b/src/EntityService.lua new file mode 100644 index 0000000..8ca90e5 --- /dev/null +++ b/src/EntityService.lua @@ -0,0 +1,190 @@ +--[[ +This matches active prototypes to create a mapping between entity names +and service (group) names. + +It also has a routine to re-scan the surfaces using that entity name map. +FIXME: this didn't catch all fuel-using entities (burner-inserter in vanilla) +Add back scan from GlobalState. +]] +local M = {} + +--[[ +-- key=entity.name, val=service_type +local name_to_service = { + ["network-chest"] = "network-chest", + ["network-chest-provider"] = "network-chest-provider", + ["network-chest-requester"] = "network-chest-requester", + ["network-tank"] = "network-tank", + ["network-tank-provider"] = "network-tank-provider", + ["network-tank-requester"] = "network-tank-requester", + ["entity-ghost"] = "entity-ghost", + ["tile-ghost"] = "tile-ghost", +} + +-- key=entity.type, val=service_type +local etype_to_service = { + ["ammo-turret"] = "general-service", -- "ammo-turret", + ["artillery-turret"] = "general-service", -- "artillery-turret", + ["assembling-machine"] = "assembling-machine", + ["car"] = "car", + ["locomotive"] = "general-service", + ["boiler"] = "general-service", + ["burner-generator"] = "general-service", + ["entity-ghost"] = "entity-ghost", + ["furnace"] = "furnace", + ["lab"] = "lab", + ["rocket-silo"] = "rocket-silo", + ["spider-vehicle"] = "spidertron", + ["mining-drill"] = "general-service", + ["reactor"] = "reactor", +} + +-- key=service, val=filter +local filter_to_service = { + ["car"] = { filter = "type", type = "car" }, + ["boiler"] = { filter = "type", type = "boiler" }, + ["burner-generator"] = { filter = "type", type = "burner-generator" }, + ["furnace"] = { filter = "type", type = "furnace" }, + ["mining-drill"] = { filter = "type", type = "mining-drill" }, + ["artillery-turret"] = { filter = "type", type = "artillery-turret" }, + ["ammo-turret"] = { filter = "type", type = "ammo-turret" }, + ["assembling-machine"] = { filter = "type", type = "assembling-machine" }, + ["lab"] = { filter = "type", type = "lab" }, + ["sink-chest"] = { filter = "name", name = "arr-hidden-sink-chest" }, + ["sink-tank"] = { filter = "name", name = "arr-sink-tank" }, + ["logistic-sink-chest"] = { filter = "name", name = "arr-logistic-sink-chest" }, + ["logistic-requester-chest"] = { filter = "name", name = "arr-logistic-requester-chest" }, + ["arr-requester-tank"] = { filter = "name", name = "arr-requester-tank" }, + ["spidertron"] = { filter = "type", type = "spider-vehicle" }, + ["reactor"] = { filter = "type", type = "reactor" }, + ["arr-combinator"] = { filter = "name", name = "arr-combinator" }, +} + +function M.calculate_groups() + local names_to_groups = {} + + -- copy over direct name-to-service maps + for name, service in pairs(name_to_service) do + if game.entity_prototypes[name] ~= nil then + names_to_groups[name] = service + else + print(string.format("MISSING: %s", name)) + end + end + + -- search type-to-service maps + for etype, service in pairs(etype_to_service) do + local entity_prototypes = game.get_filtered_entity_prototypes({{ filter="type", type=etype }}) + for name, _ in pairs(entity_prototypes) do + names_to_groups[name] = service + end + end + + -- generic filters + for service, filter in pairs(filter_to_service) do + local entity_prototypes = game.get_filtered_entity_prototypes({ filter }) + for name, _ in pairs(entity_prototypes) do + names_to_groups[name] = service + end + end + + -- logistic chests + local entity_prototypes = game.get_filtered_entity_prototypes({{ filter="type", type="logistic-container" }}) + for _, prot in pairs(entity_prototypes) do + names_to_groups[prot.name] = "logistic-chest-" .. prot.logistic_mode + end + + return names_to_groups +end +]] + +--[[ +Scan prototypes and calculate a mapping of entity.name to service name. +]] +function M.scan_prototypes() + -- fixed list of mappings + -- key=entity_name, val=service_type + local fixed_name_to_service = { + ["network-chest"] = "network-chest", + ["network-chest-provider"] = "network-chest-provider", + ["network-chest-requester"] = "network-chest-requester", + ["network-tank"] = "network-tank", + ["network-tank-provider"] = "network-tank-provider", + ["network-tank-requester"] = "network-tank-requester", + ["entity-ghost"] = "entity-ghost", + ["tile-ghost"] = "tile-ghost", + } + + -- type to service mappings + -- key=type, val=service_type + local etype_to_service = { + ["ammo-turret"] = "general-service", -- "ammo-turret", + ["artillery-turret"] = "general-service", -- "artillery-turret", + ["assembling-machine"] = "assembling-machine", + ["car"] = "car", + ["entity-ghost"] = "entity-ghost", + ["furnace"] = "furnace", + ["lab"] = "lab", + ["rocket-silo"] = "rocket-silo", + ["spider-vehicle"] = "spidertron", + ["reactor"] = "reactor", + } + + local name_to_service = {} + for name, service in pairs(fixed_name_to_service) do + if game.entity_prototypes[name] ~= nil then + name_to_service[name] = service + end + end + + for _, prot in pairs(game.entity_prototypes) do + -- discard hidden entities and those that can't be placed by a player + if prot.has_flag("hidden") or not prot.has_flag("player-creation") then + -- not adding it, not sure what this skips anymore + + elseif prot.type == "logistic-container" then + -- special handler for logistic containers + name_to_service[prot.name] = "logistic-chest-" .. prot.logistic_mode + + else + -- see if we handle the type + local ss = etype_to_service[prot.type] + if ss ~= nil then + name_to_service[prot.name] = ss + else + -- check for other 'general-service' + local svc_type + -- check for stuff that burns chemicals (coal) + if prot.burner_prototype ~= nil and prot.burner_prototype.fuel_categories.chemical == true then + svc_type = "general-service" -- for refueling + --clog("Adding %s based on burner_prototype", prot.name) + end + if svc_type ~= nil then + name_to_service[prot.name] = svc_type + end + end + end + end + log(("Servicing %s prototypes"):format(table_size(name_to_service))) + log(serpent.block(name_to_service)) + return name_to_service +end + +-- called once at startup if scan_prototypes() returns something different +function M.scan_surfaces() + print(string.format("[%s] cheat-network: Scanning surfaces", game.tick)) + local name_filter = {} + for name, _ in pairs(global.name_service_map) do + table.insert(name_filter, name) + end + for _, surface in pairs(game.surfaces) do + local entities = surface.find_entities_filtered { name = name_filter } + for _, ent in ipairs(entities) do + M.entity_info_add(ent) + end + end + M.reset_queues() + print(string.format("[%s] cheat-network: Scanning complete", game.tick)) +end + +return M diff --git a/src/GUICommon.lua b/src/GUICommon.lua new file mode 100644 index 0000000..d2ae2a1 --- /dev/null +++ b/src/GUICommon.lua @@ -0,0 +1,86 @@ +local GUICommon = {} +--local Util = require "src.Util" +--local Storage = require "src.Storage" + +-- "cin" stands for Cheat Item Network +GUICommon.GUI_LOGO_BUTTON = "cin-logo-button" +GUICommon.GUI_RESOURCE_TABLE = "cin-res-tables" +GUICommon.GUI_LIMIT_DIALOG = "cin-limit-diag" +GUICommon.GUI_ITEM_PRIORITY = "cin-priority-list" +GUICommon.GUI_REQUESTER_TANK = "cin-requester-tank" +GUICommon.GUI_ENTITY_PANEL = "cin-entity-panel" + +local mouse_button_str = { + [defines.mouse_button_type.left] = "left", + [defines.mouse_button_type.right] = "right", + [defines.mouse_button_type.middle] = "middle", +} + +-- awful hack to export res table function, this will be assigned in GUIResourceList +GUICommon.get_or_create_reslist_button = nil + +function GUICommon.get_click_str(event) + local str = "" + if event.control then + str = "control-" .. str + end + if event.shift then + str = "shift-" .. str + end + if event.alt then + str = "alt-" .. str + end + return str .. (mouse_button_str[event.button] or "none") +end + +function GUICommon.create_item_button(parent, storage_key, new_attrs) + local fluid_name = Storage.unpack_fluid_item_name(storage_key) + local default_attrs = { + type = "sprite-button", + sprite = fluid_name and "fluid/" .. fluid_name or "item/" .. storage_key, + } + if new_attrs.elem_tooltip == true then + new_attrs.elem_tooltip = { + type = fluid_name and "fluid" or "item", + name = fluid_name or storage_key + } + end + return parent.add(Util.table_merge(default_attrs, new_attrs)) +end + +function GUICommon.create_header(parent, title, close_event) + local header_flow = parent.add({ + type = "flow", + direction = "horizontal", + }) + header_flow.drag_target = parent + + header_flow.add { + type = "label", + caption = title, + style = "frame_title", + ignored_by_interaction = true, + } + + local header_drag = header_flow.add { + type = "empty-widget", + style = "draggable_space_header", + ignored_by_interaction = true, + } + header_drag.style.height = 24 + header_drag.style.horizontally_stretchable = true + header_drag.style.vertically_stretchable = true + + header_flow.add { + type = "sprite-button", + sprite = "utility/close_white", + hovered_sprite = "utility/close_black", + clicked_sprite = "utility/close_black", + style = "cancel_close_button", + tags = { event = close_event }, + } + + return header_flow +end + +return GUICommon diff --git a/src/GUIComponentSliderInput.lua b/src/GUIComponentSliderInput.lua new file mode 100644 index 0000000..84dabcb --- /dev/null +++ b/src/GUIComponentSliderInput.lua @@ -0,0 +1,125 @@ +local GUIComponentSliderInput = {} +local flib_table = require("__flib__/table") +local Util = require "src.Util" +local GUIDispatcher = require "src.GUIDispatcher" + +local SLIDER_EVENT = "arr-component-slider" +local INPUT_EVENT = "arr-component-slider-input" + +local function get_closest_step_index(value, step_values) + local min_i = 1 + local min_dist = math.huge + for i, step_val in ipairs(step_values) do + local dist = math.abs(value - step_val) + if dist <= min_dist then + min_dist = dist + min_i = i + end + end + return min_i +end + +local function remove_duplicates(array) + local seen = {} + local out = {} + for i, val in ipairs(array) do + if not seen[val] then + table.insert(out, val) + seen[val] = true + end + end + return out +end + +local function scale_array(array, scale, min, max) + local out = {} + for i, val in ipairs(array) do + table.insert( + out, + Util.clamp(math.ceil(val * scale), min or -math.huge, max or math.huge) + ) + end + return out +end + +function GUIComponentSliderInput.set_limits(parent, min_val, max_val) + local slider = parent.slider + local input = parent.input + slider.tags = Util.table_merge(slider.tags, { min = min_val, max = max_val }) + input.tags = Util.table_merge(input.tags, { min = min_val, max = max_val }) + if slider.tags.steps then + slider.set_slider_minimum_maximum(1, #slider.tags.steps) + else + slider.set_slider_minimum_maximum(min_val, max_val) + end +end + +function GUIComponentSliderInput.create(parent, slider_attrs, input_attrs, slider_steps, slider_mult, min_val, max_val) + local value = slider_attrs.value or 0 + if slider_steps then + slider_steps = remove_duplicates(scale_array(slider_steps, slider_mult or 1, min_val, max_val)) + slider_attrs.value_step = 1 + end + local slider = parent.add(flib_table.deep_merge({ + { + type = "slider", + name = "slider", + style = "notched_slider", + tags = { + event = { [SLIDER_EVENT] = true }, + steps = slider_steps, + } + }, + slider_attrs + })) + local input = parent.add(flib_table.deep_merge({ + { + type = "textfield", + name = "input", + style = "slider_value_textfield", + text = value, + numeric = true, + allow_decimal = false, + tags = { event = { [INPUT_EVENT] = true } } + }, + input_attrs + })) + + GUIComponentSliderInput.set_limits( + parent, + min_val or slider.get_slider_minimum(), + max_val or slider.get_slider_maximum() + ) + if slider_steps then + slider.slider_value = get_closest_step_index(value, slider_steps) + end +end + +function GUIComponentSliderInput.set_value(parent, value) + local slider = parent.slider + local input = parent.input + local new_value = Util.clamp(value or 0, input.tags.min, input.tags.max) + input.text = tostring(new_value) + if slider.tags.steps then + new_value = get_closest_step_index(new_value, slider.tags.steps) + end + slider.slider_value = new_value +end + +local function on_slider_changed(event, tags, player) + local new_value = event.element.slider_value + local input = event.element.parent.input + if tags.steps then + new_value = Util.clamp(tags.steps[new_value], tags.min, tags.max) + end + input.text = tostring(new_value) +end + +local function on_text_changed(event, tags, player) + GUIComponentSliderInput.set_value(event.element.parent, tonumber(event.element.text) or 0) +end + +GUIDispatcher.register(defines.events.on_gui_value_changed, SLIDER_EVENT, on_slider_changed) +GUIDispatcher.register(defines.events.on_gui_text_changed, INPUT_EVENT, on_text_changed) + +return GUIComponentSliderInput diff --git a/src/GUIDispatcher.lua b/src/GUIDispatcher.lua new file mode 100644 index 0000000..4ebadcc --- /dev/null +++ b/src/GUIDispatcher.lua @@ -0,0 +1,92 @@ +--[[ +Helper that dispatches GUI events based on a supplied tag. +Slightly easier to use than '__stdlib__/stdlib/event/gui' +]] +local Event = require('__stdlib__/stdlib/event/event') +local table = require('__stdlib__/stdlib/utils/table') + +local GUIDispatcher = {} + +GUIDispatcher.ON_CONFIRM_KEYPRESS = "arr-gui-confirm" +GUIDispatcher.ON_COPY_SETTINGS_KEYPRESS = "arr-copy-entity-settings" +GUIDispatcher.ON_COPY_CONDITIONS_KEYPRESS = "arr-copy-entity-conditions" + +-- REVISIT: don't add stuff until register is called? +-- { on_click = { event_tag = fn, ... }, ... } +local registered_tagged_events = { + [defines.events.on_gui_click] = {}, + [defines.events.on_gui_closed] = {}, + [defines.events.on_gui_value_changed] = {}, + [defines.events.on_gui_text_changed] = {}, + [defines.events.on_gui_elem_changed] = {}, + [defines.events.on_gui_checked_state_changed] = {}, + [defines.events.on_gui_selection_state_changed] = {}, + + [GUIDispatcher.ON_CONFIRM_KEYPRESS] = {}, + [GUIDispatcher.ON_COPY_SETTINGS_KEYPRESS] = {}, + [GUIDispatcher.ON_COPY_CONDITIONS_KEYPRESS] = {}, + + [defines.events.on_gui_confirmed] = {}, + [defines.events.on_gui_opened] = {}, + [defines.events.on_gui_location_changed] = {}, +} +-- { on_click = { fn1, fn2, ... }, ... } +local registered_events = table.deepcopy(registered_tagged_events) + +function GUIDispatcher.register(event_name, event_tag, handler) + if event_tag then + registered_tagged_events[event_name][event_tag] = handler + else + table.insert(registered_events[event_name], handler) + end +end + +function GUIDispatcher.on_event(event) + local player = game.get_player(event.player_index) + local tags = (event.element or {}).tags or {} + local event_name = event.input_name or event.name + local event_tags = tags['event'] + local fired_handlers = {} + + -- convert a simple tag to a table: { event = "foo" } => { event = { "foo" = true } } + if type(event_tags) == "string" then + event_tags = { [event_tags] = true } + end + + if event_tags then + for event_tag, _ in pairs(event_tags) do + local handler = registered_tagged_events[event_name][event_tag] + if handler then + handler(event, tags, player) + fired_handlers[handler] = true + end + end + end + + -- fire all handlers that accept all events + for event_tag, fn in pairs(registered_events[event_name]) do + if fired_handlers[fn] == nil then + fn(event, nil, player) + end + end +end + +-- Register events handled by GUIDispatcher +Event.on_event(defines.events.on_gui_opened, GUIDispatcher.on_event) +Event.on_event(defines.events.on_gui_closed, GUIDispatcher.on_event) + +Event.on_event(defines.events.on_gui_click, GUIDispatcher.on_event) +Event.on_event(defines.events.on_gui_value_changed, GUIDispatcher.on_event) +Event.on_event(defines.events.on_gui_text_changed, GUIDispatcher.on_event) +Event.on_event(defines.events.on_gui_elem_changed, GUIDispatcher.on_event) +Event.on_event(defines.events.on_gui_checked_state_changed, GUIDispatcher.on_event) +Event.on_event(defines.events.on_gui_confirmed, GUIDispatcher.on_event) +Event.on_event(defines.events.on_gui_selection_state_changed, GUIDispatcher.on_event) +Event.on_event(defines.events.on_gui_location_changed, GUIDispatcher.on_event) + +-- custom events +--Event.on_event(GUIDispatcher.ON_CONFIRM_KEYPRESS, GUIDispatcher.on_event) +--Event.on_event(GUIDispatcher.ON_COPY_SETTINGS_KEYPRESS, GUIDispatcher.on_event) +--Event.on_event(GUIDispatcher.ON_COPY_CONDITIONS_KEYPRESS, GUIDispatcher.on_event) + +return GUIDispatcher diff --git a/src/GUIEntityPanel.lua b/src/GUIEntityPanel.lua new file mode 100644 index 0000000..3dc76ac --- /dev/null +++ b/src/GUIEntityPanel.lua @@ -0,0 +1,532 @@ +local GUIEntityPanel = {} + +local flib_table = require("__flib__/table") +local EntityCondition = require "src.EntityCondition" +local EntityGroups = require "src.EntityService" +local FurnaceRecipeManager = require "src.FurnaceRecipeManager" +local GUICommon = require "src.GUICommon" +local GUIComponentSliderInput = require "src.GUIComponentSliderInput" +local GUIDispatcher = require "src.GUIDispatcher" +local GUIItemPriority = require "src.GUIItemPriority" +local ItemPriorityManager = require "src.ItemPriorityManager" +local R = require "src.RichText" +local Storage = require "src.Storage" +local Util = require "src.Util" + +local GUI_CLOSE_EVENT = "arr-entity-panel-close" +local PRIORITISE_CHECKED_EVENT = "arr-entity-panel-prioritise" +local CONDITION_ITEM_EVENT = "arr-entity-panel-condition-item" +local CONDITION_OP_EVENT = "arr-entity-panel-condition-op" +local CONDITION_VALUE_BUTTON_EVENT = "arr-entity-panel-condition-button" +local CONDITION_VALUE_CHANGED_EVENT = "arr-entity-panel-condition-value-changed" +local CONDITION_SURFACE_CHANGED_EVENT = "arr-entity-panel-condition-surface-changed" +local CONDITION_SURFACE_RESET_EVENT = "arr-entity-panel-condition-surface-reset" +local FURNACE_RECIPE_EVENT = "arr-entity-panel-furnace-recipe" +local RETURN_EXCESS_CHECKED_EVENT = "arr-entity-panel-return-excess" +local SHOW_PRIORITY_GUI_EVENT = "arr-entity-panel-show-priority-gui" + +local EntityTypeGUIAnchors = { + ["assembling-machine"] = defines.relative_gui_type.assembling_machine_gui, + ["car"] = defines.relative_gui_type.car_gui, + ["logistic-container"] = defines.relative_gui_type.container_gui, + ["furnace"] = defines.relative_gui_type.furnace_gui, + ["lab"] = defines.relative_gui_type.lab_gui, + ["mining-drill"] = defines.relative_gui_type.mining_drill_gui, + ["boiler"] = defines.relative_gui_type.entity_with_energy_source_gui, + ["burner-generator"] = defines.relative_gui_type.entity_with_energy_source_gui, + ["artillery-turret"] = defines.relative_gui_type.container_gui, + ["ammo-turret"] = defines.relative_gui_type.container_gui, + ["reactor"] = defines.relative_gui_type.reactor_gui, + ["storage-tank"] = defines.relative_gui_type.storage_tank_gui, + ["rocket-silo"] = defines.relative_gui_type.rocket_silo_gui, + ["spider-vehicle"] = defines.relative_gui_type.spider_vehicle_gui, + ["constant-combinator"] = defines.relative_gui_type.constant_combinator_gui, +} + +function GUIEntityPanel.initialise() + if global.entity_panel_pending_relocations == nil then + global.entity_panel_pending_relocations = {} + end + if global.entity_panel_location == nil then + global.entity_panel_location = {} + end +end + +local function get_location_key(player, entity_name) + return ("%d;%s;%s;%s"):format( + player.index, + player.opened.get_mod(), + entity_name, + player.opened.name + ) +end + +local function add_panel_frame(parent, caption, tooltip) + parent.add({ + type = "line", + style = "control_behavior_window_line" + }) + local frame = parent.add({ + type = "frame", + style = "invisible_frame", + direction = "vertical" + }) + local label = frame.add({ + type = "label", + style = "heading_2_label", + caption = caption, + tooltip = tooltip + }) + label.style.padding = { 4, 0, 4, 0 } + return frame +end + +local function add_gui_content(window, entity) + local frame = window.add({ + type = "frame", + style = "inside_shallow_frame_with_padding", + direction = "vertical" + }) + + local data_id = entity.unit_number + -- Prioritise + local data = global.entity_data[data_id] + if not data then + data = {} + global.entity_data[data_id] = data + end + frame.add({ + type = "checkbox", + caption = "Prioritise [img=info]", + tooltip = "Allow consumption of reserved resources", + state = (data.use_reserved == true), + tags = { id = data_id, event = PRIORITISE_CHECKED_EVENT } + }) + frame.add({ + type = "line", + style = "control_behavior_window_line" + }) + + -- Condition + if not data.condition then + data.condition = {} + end + local condition = data.condition + local condition_value = condition.value or 0 + local condition_frame = frame.add({ + type = "frame", + name = "condition_frame", + style = "invisible_frame_with_title", + caption = { "gui-control-behavior-modes-guis.enabled-condition" }, + direction = "vertical" + }) + local condition_controls_flow = condition_frame.add({ + type = "flow", + name = "condition_controls_flow", + direction = "horizontal" + }) + condition_controls_flow.style.vertical_align = "center" + local fluid_name = Storage.unpack_fluid_item_name(condition.item or "") + condition_controls_flow.add({ + type = "choose-elem-button", + elem_type = "signal", + style = "slot_button_in_shallow_frame", + signal = { + type = fluid_name and "fluid" or "item", + name = fluid_name or condition.item + }, + tags = { id = data_id, event = CONDITION_ITEM_EVENT } + }) + condition_controls_flow.add({ + type = "drop-down", + items = EntityCondition.OPERATIONS, + selected_index = condition.op or 1, + style = "circuit_condition_comparator_dropdown", + tags = { id = data_id, event = CONDITION_OP_EVENT } + }) + local condition_value_btn = condition_controls_flow.add({ + type = "button", + name = "condition_value", + style = "slot_button_in_shallow_frame", + caption = condition_value .. "%", + tags = { event = CONDITION_VALUE_BUTTON_EVENT } + }) + condition_value_btn.style.font_color = { 1, 1, 1 } + + local condition_slider_flow = condition_frame.add({ + type = "flow", + name = "slider_flow", + style = "player_input_horizontal_flow", + }) + condition_slider_flow.style.top_padding = 4 + condition_slider_flow.visible = false + GUIComponentSliderInput.create( + condition_slider_flow, + { + value = condition_value, + maximum_value = 100, + style = "slider", + tags = { id = data_id, event = { [CONDITION_VALUE_CHANGED_EVENT] = true } } + }, + { + allow_negative = false, + style = "very_short_number_textfield", + tags = { id = data_id, event = { [CONDITION_VALUE_CHANGED_EVENT] = true } } + } + ) + condition_slider_flow.slider.style.width = 100 + condition_slider_flow.add({ + type = "label", + caption = "%", + }) + + local surface_names = Util.table_keys(game.surfaces) + local current_surface = condition.surface or entity.surface.name + local current_surface_i = flib_table.find(surface_names, current_surface) + if current_surface_i == nil then + table.insert(surface_names, ("%s%s (not found)%s"):format(R.COLOUR_RED, current_surface, R.COLOUR_END)) + current_surface_i = #surface_names + end + if table_size(surface_names) > 1 then + local condition_surface_flow = condition_frame.add({ + type = "flow", + direction = "horizontal" + }) + condition_surface_flow.style.vertical_align = "center" + + condition_surface_flow.add({ + type = "label", + style = "heading_2_label", + caption = "Storage source [img=info]", + tooltip = "The surface of the storage to use when checking the condition, and reading signals (for [entity=arr-combinator] Auto Resource Combinators)" + }) + + local button = condition_surface_flow.add({ + type = "sprite-button", + resize_to_sprite = false, + sprite = "utility/reset_white", + tooltip = "Click to reset to current surface", + tags = { id = data_id, event = CONDITION_SURFACE_RESET_EVENT } + }) + button.style.size = { 24, 24 } + + condition_frame.add({ + type = "drop-down", + name = "condition_surface", + items = surface_names, + selected_index = current_surface_i, + tags = { id = data_id, event = CONDITION_SURFACE_CHANGED_EVENT } + }) + end + + if entity.name == "arr-logistic-requester-chest" then + local sub_frame = add_panel_frame(frame, "Requester Chest") + frame.add({ + type = "checkbox", + caption = "Return excess items [img=info]", + tooltip = "Return items that are not requested or are above the requested amount", + state = (data.return_excess == true), + tags = { id = data_id, event = RETURN_EXCESS_CHECKED_EVENT } + }) + end + + if entity.type == "furnace" then + local sub_frame = add_panel_frame( + frame, + { "", { "description.recipe" }, " [img=info]" }, + "The new recipe will be applied on the next production cycle, when the productivity bar is empty." + ) + local current_recipe = FurnaceRecipeManager.get_recipe(entity) + local filters = {} + for category, _ in pairs(entity.prototype.crafting_categories) do + table.insert(filters, { filter = "category", category = category }) + end + sub_frame.add({ + type = "choose-elem-button", + elem_type = "recipe", + style = "slot_button_in_shallow_frame", + recipe = current_recipe and current_recipe.name or nil, + elem_filters = filters, + tags = { id = data_id, event = FURNACE_RECIPE_EVENT } + }) + end + + local priority_sets = ItemPriorityManager.get_priority_sets_for_entity(entity) + -- { [group] = { set1_key, set2_key, ... } } + local related_priority_set_keys = {} + for set_key, priority_set in pairs(priority_sets) do + if priority_set.group then + local sets = related_priority_set_keys[priority_set.group] or {} + table.insert(sets, set_key) + related_priority_set_keys[priority_set.group] = sets + end + end + if table_size(related_priority_set_keys) > 0 then + local sub_frame = add_panel_frame( + frame, + "Item Priority [img=info]", + { + "", ("Effects every [entity=%s] "):format(entity.name), entity.localised_name, + "\nItems are used from left to right.\n", + R.HINT, { "control-keys.mouse-button-1" }, R.HINT_END, " to set item quantity.", + } + ) + sub_frame.style.vertically_stretchable = false + local inner_flow = sub_frame.add({ + type = "flow", + direction = "vertical" + }) + inner_flow.style.left_margin = 4 + inner_flow.style.vertical_spacing = 0 + for group, set_keys in pairs(related_priority_set_keys) do + local label_flow = inner_flow.add({ + type = "flow", + direction = "horizontal" + }) + label_flow.style.vertical_align = "center" + + local label = label_flow.add({ + type = "label", + style = "heading_2_label", + caption = group, + }) + label.style.bottom_padding = 0 + + local button = label_flow.add({ + type = "sprite-button", + resize_to_sprite = false, + sprite = "arr-logo", + tooltip = "Click to show in the full priority list window", + tags = { + event = SHOW_PRIORITY_GUI_EVENT, + group = group, + entity = entity.name, + } + }) + button.style.size = { 24, 24 } + + for _, set_key in ipairs(set_keys) do + local flow = inner_flow.add({ + type = "flow", + }) + GUIComponentItemPrioritySet.create(flow, priority_sets, set_key, 6) + end + end + end +end + +local function close_gui(player) + local last_position = nil + local window = player.gui.relative[GUICommon.GUI_ENTITY_PANEL] + if window then + window.destroy() + end + + window = player.gui.screen[GUICommon.GUI_ENTITY_PANEL] + if window then + last_position = window.location + window.destroy() + end + return last_position +end + +local function on_gui_opened(event, tags, player) + local last_position = close_gui(player) + local entity = event.entity + if not entity or not EntityGroups.can_manage(entity) then + return + end + + -- if .opened is a custom UI, then open on screen instead because we can't anchor + if player.opened and player.opened.object_name == "LuaGuiElement" then + local parent = player.gui.screen + local window = parent.add({ + type = "frame", + name = GUICommon.GUI_ENTITY_PANEL, + direction = "vertical", + style = "inner_frame_in_outer_frame", + tags = { entity_name = entity.name } + }) + GUICommon.create_header(window, "Cheat Network", M.GUI_CLOSE_EVENT) + -- use the location from the previous time a similar UI was opened + -- this might be incorrect, but should be corrected on the next tick + local location_key = get_location_key(player, entity.name) + last_position = global.entity_panel_location[location_key] or last_position + local res = player.display_resolution + if last_position and last_position.x + 100 < res.width and last_position.y + 100 < res.height then + window.location = last_position + else + window.force_auto_center() + end + -- the position of the parent GUI will only be known on the next tick + -- so set a flag for on_tick to reposition us later + global.entity_panel_pending_relocations[player.index] = { + player = player, + tick = game.tick + 1 + } + add_gui_content(window, entity) + return + end + + local anchor = EntityTypeGUIAnchors[entity.type] + if not anchor then + log(("FIXME: don't know how to anchor to entity GUI name=%s type=%s"):format(entity.name, entity.type)) + return + end + + local relative = player.gui.relative + local window = relative[GUICommon.GUI_ENTITY_PANEL] + window = relative.add({ + type = "frame", + name = GUICommon.GUI_ENTITY_PANEL, + direction = "vertical", + style = "inner_frame_in_outer_frame", + anchor = { + position = defines.relative_gui_position.right, + gui = anchor, + }, + caption = "Cheat Network", + tags = { entity_id = entity.unit_number } + }) + add_gui_content(window, entity) +end + +local function on_gui_closed(event, tags, player) + close_gui(player) +end + +function GUIEntityPanel.on_tick() + -- assume mod UI is centered and reposition our panel based on the guessed width + for player_id, t in pairs(global.entity_panel_pending_relocations) do + if game.tick < t.tick then + goto continue + end + if t.player.opened then + local window = t.player.gui.screen[GUICommon.GUI_ENTITY_PANEL] + local parent_location = t.player.opened.location + local tags = window.tags + -- remember the location, so we can use it for the initial frame when a similar UI opens + -- we don't account for the parent size, so it might positioned incorrectly + -- but it is less distracting than being in the center + local basic_location_key = get_location_key(t.player, tags.entity_name) + tags.location_key = ("%s;%d;%d"):format(basic_location_key, parent_location.x, parent_location.y) + window.tags = tags + local previous_location = global.entity_panel_location[tags.location_key] + if previous_location then + window.location = previous_location + global.entity_panel_location[basic_location_key] = previous_location + else + local res = t.player.display_resolution + local guessed_width = (res.width / 2 - parent_location.x) * 2 + if guessed_width > 0 then + window.location = { parent_location.x + guessed_width, parent_location.y } + global.entity_panel_location[basic_location_key] = window.location + end + end + end + global.entity_panel_pending_relocations[player_id] = nil + ::continue:: + end +end + +function GUIEntityPanel.on_location_changed(event) + if event.element.name == GUICommon.GUI_ENTITY_PANEL then + local location_key = event.element.tags.location_key + if location_key then + global.entity_panel_location[location_key] = event.element.location + end + end +end + +local function on_prioritise_checked(event, tags, player) + global.entity_data[tags.id].use_reserved = event.element.state +end + +local function on_condition_item_changed(event, tags, player) + local signal = event.element.elem_value + local storage_key = signal and (signal.type == "fluid" and Storage.get_fluid_storage_key(signal.name) or signal.name) + if signal and signal.type == "virtual" then + event.element.elem_value = nil + return + end + global.entity_data[tags.id].condition.item = storage_key +end + +local function on_condition_op_changed(event, tags, player) + global.entity_data[tags.id].condition.op = event.element.selected_index +end + +local function on_condition_value_clicked(event, tags, player) + local slider_flow = event.element.parent.parent.slider_flow + event.element.toggled = not event.element.toggled + slider_flow.visible = event.element.toggled +end + +local function on_condition_value_changed(event, tags, player) + local new_value = event.element.parent.input.text + local condition_controls_flow = event.element.parent.parent.condition_controls_flow + condition_controls_flow.condition_value.caption = new_value .. "%" + global.entity_data[tags.id].condition.value = tonumber(new_value) +end + +local function on_condition_value_confirmed(event, tags, player) + local condition_controls_flow = event.element.parent.parent.condition_controls_flow + local slider_flow = event.element.parent.parent.slider_flow + condition_controls_flow.condition_value.toggled = false + slider_flow.visible = false +end + +local function on_condition_surface_changed(event, tags, player) + local surface_name = event.element.items[event.element.selected_index] + global.entity_data[tags.id].condition.surface = surface_name +end + +local function on_condition_surface_reset_clicked(event, tags, player) + local dropdown = event.element.parent.parent.condition_surface + local entity = global.entities[tags.id] + dropdown.selected_index = flib_table.find(dropdown.items, entity.surface.name) + global.entity_data[tags.id].condition.surface = nil +end + +local function on_furnace_recipe_changed(event, tags, player) + local new_recipe_name = event.element.elem_value + local entity = global.entities[tags.id] + if not new_recipe_name then + local recipe = FurnaceRecipeManager.get_recipe(entity) + event.element.elem_value = recipe and recipe.name + return + end + FurnaceRecipeManager.set_recipe(entity, new_recipe_name) +end + +local function on_return_excess_checked(event, tags, player) + global.entity_data[tags.id].return_excess = event.element.state +end + +local function on_show_priority_gui(event, tags, player) + GUIItemPriority.open(player, tags.group, ItemPriorityManager.get_mapped_entity_name(tags.entity)) +end + +GUIDispatcher.register(defines.events.on_gui_click, GUI_CLOSE_EVENT, on_gui_closed) + +GUIDispatcher.register(defines.events.on_gui_opened, nil, on_gui_opened) +GUIDispatcher.register(defines.events.on_gui_closed, nil, on_gui_closed) + +GUIDispatcher.register(defines.events.on_gui_checked_state_changed, PRIORITISE_CHECKED_EVENT, on_prioritise_checked) + +GUIDispatcher.register(defines.events.on_gui_elem_changed, CONDITION_ITEM_EVENT, on_condition_item_changed) +GUIDispatcher.register(defines.events.on_gui_selection_state_changed, CONDITION_OP_EVENT, on_condition_op_changed) +GUIDispatcher.register(defines.events.on_gui_click, CONDITION_VALUE_BUTTON_EVENT, on_condition_value_clicked) +GUIDispatcher.register(defines.events.on_gui_value_changed, CONDITION_VALUE_CHANGED_EVENT, on_condition_value_changed) +GUIDispatcher.register(defines.events.on_gui_text_changed, CONDITION_VALUE_CHANGED_EVENT, on_condition_value_changed) +GUIDispatcher.register(defines.events.on_gui_confirmed, CONDITION_VALUE_CHANGED_EVENT, on_condition_value_confirmed) +GUIDispatcher.register(defines.events.on_gui_selection_state_changed, CONDITION_SURFACE_CHANGED_EVENT, on_condition_surface_changed) +GUIDispatcher.register(defines.events.on_gui_click, CONDITION_SURFACE_RESET_EVENT, on_condition_surface_reset_clicked) + +GUIDispatcher.register(defines.events.on_gui_elem_changed, FURNACE_RECIPE_EVENT, on_furnace_recipe_changed) + +GUIDispatcher.register(defines.events.on_gui_checked_state_changed, RETURN_EXCESS_CHECKED_EVENT, on_return_excess_checked) + +GUIDispatcher.register(defines.events.on_gui_click, SHOW_PRIORITY_GUI_EVENT, on_show_priority_gui) + +return GUIEntityPanel diff --git a/src/GlobalState.lua b/src/GlobalState.lua index db4ed07..1d959e8 100644 --- a/src/GlobalState.lua +++ b/src/GlobalState.lua @@ -3,8 +3,8 @@ This is the do-everything dumping ground for stuff that touches 'global'. TODO: split this up by functionality. ]] -local tables_have_same_keys = require("src.tables_have_same_keys") - .tables_have_same_keys +local thsk = require("src.tables_have_same_keys") +local EntityService = require("src.EntityService") local constants = require "src.constants" local clog = require("src.log_console").log local Event = require('__stdlib__/stdlib/event/event') @@ -17,8 +17,14 @@ local setup_has_run = false function M.setup() if setup_has_run then + -- delay the scan for a tick + if global.need_to_scan == true then + global.need_to_scan = nil + M.scan_surfaces() + end return end + log("Running GlobalState.setup()") setup_has_run = true M.reread_settings() @@ -113,8 +119,8 @@ It should set up the global data structure. ]] function M.inner_setup() -- always rebuild the ammo and fuel tables (for now) - global.ammo_table = nil - global.fuel_table = nil + --global.ammo_table = nil + --global.fuel_table = nil -- the 'mod' table holds most interesting data if global.mod == nil then @@ -133,6 +139,9 @@ function M.inner_setup() -- reset the service queue if the size changes or this is the first call if global.mod.scan_queues == nil or (#global.mod.scan_queues ~= constants.QUEUE_COUNT) then + if global.mod.scan_queues ~= nil then + print("resetting queues due to size change") + end M.reset_queues() end -- M.log_queue_info() @@ -146,7 +155,9 @@ function M.inner_setup() if global.mod.fluids == nil then global.mod.fluids = {} end - M.normalize_fluids() + + -- M.normalize_fluids() + if global.mod.missing_item == nil then global.mod.missing_item = {} -- missing_item[item][unit_number] = { game.tick, count } end @@ -169,17 +180,24 @@ function M.inner_setup() end -- scan prototypes to see if we need to rescan all surfaces - local name_service_map = M.scan_prototypes() - if true or not tables_have_same_keys(name_service_map, global.name_service_map) then - clog("*** PROTOTYPES CHANGED ***") + + local name_service_map = EntityService.scan_prototypes() + if thsk.same_value(name_service_map, global.name_service_map) then + log("no prototype changes") + else + log("Prototype changes; scanning") global.name_service_map = name_service_map - M.scan_surfaces() + global.need_to_scan = true end + -- major upgrade to unified entity info table, trashes all the separate tables + --[[ if global.mod.chests ~= nil then M.convert_to_entity_info() + global.mod.chests = nil end + ]] if global.mod.alert_trans == nil then global.mod.alert_trans = {} -- alert_trans[unit_number] = game.tick @@ -193,7 +211,13 @@ end function M.reread_settings() global.infinite_supply = settings.global["item-network-cheat-infinite-duplicate"].value global.production_supply = settings.global["item-network-cheat-production-duplicate"].value - print(string.format("item-network: SETTINGS: inf=%s prod=%s", global.infinite_supply, global.production_supply)) + --print(string.format("item-network: SETTINGS: inf=%s prod=%s", global.infinite_supply, global.production_supply)) +end + +function M.surface_scan_add(surface_index) + if surface_index == nil then + global.surfaces_to_scan = { [0] = true } + end end --[[ @@ -237,7 +261,8 @@ function M.convert_to_entity_info() } ]] - global.name_service_map = M.scan_prototypes() + print("convert_to_entity_info") + --global.name_service_map = M.scan_prototypes() -- chests need a 'service_type' field for unum, info in pairs(global.mod.chests or {}) do @@ -271,11 +296,11 @@ function M.convert_to_entity_info() end global.mod.serviced = nil - M.scan_surfaces() + global.need_to_scan = true end function M.reset_queues() - print("item-network: RESET SCAN QUEUE") + log("RESET SCAN QUEUE") global.mod.scan_deadline = 0 -- keep doing nothing until past this tick global.mod.scan_index = 1 -- working on the queue in this index @@ -511,6 +536,17 @@ function M.entity_info_set(unit_number, info) global.mod.entity_info[unit_number] = info end +function M.entity_info_config_update(unit_number, config) + if type(config) == "table" then + local info = global.mod.entity_info[unit_number] + if info ~= nil then + for k, v in pairs(config) do + info.config[k] = v + end + end + end +end + function M.entity_info_clear(unit_number) global.mod.entity_info[unit_number] = nil end @@ -528,7 +564,8 @@ function M.entity_info_add(entity, tags) if service_type == nil then return end --- only if we haven't already added this one + + -- only if we haven't already added this one local info = M.entity_info_get(unit_number) if info == nil then -- grab the service_type. bail if not handled. @@ -536,11 +573,13 @@ function M.entity_info_add(entity, tags) info = { service_type = service_type, entity = entity, + config = {}, + unit_number = entity.unit_number, } local svc_func = M.get_service_task(service_type) if svc_func == nil then - clog("genric : nothing for %s", serpent.line(info)) + clog("generic : nothing for %s", serpent.line(info)) end if svc_func ~= nil and svc_func.tag ~= nil then if tags ~= nil then @@ -1080,10 +1119,10 @@ function M.register_service_task(service_type, funcs) funcs.create = M.entity_info_add end M.service_tasks[service_type] = funcs - clog("added service %s", service_type) - for k, v in pairs(funcs) do - clog(' - %s : %s', k, type(v)) - end + log(("added service %s"):format(service_type)) + --for k, v in pairs(funcs) do + -- clog(' - %s : %s', k, type(v)) + --end end function M.get_service_task(service_type) @@ -1731,11 +1770,9 @@ end ------------------------------------------------------------------------------- function M.scan_prototypes() - clog("item-network: scanning prototypes") - -- key=entity_name, val=service_type - local name_to_service = { - -- add built-in stuff that we create + local fixed_name_to_service = { + -- add built-in stuff and stuff that we create ["network-chest"] = "network-chest", ["network-chest-provider"] = "network-chest-provider", ["network-chest-requester"] = "network-chest-requester", @@ -1759,6 +1796,13 @@ function M.scan_prototypes() ["spider-vehicle"] = "spidertron", } + local name_to_service = {} + for name, service in pairs(fixed_name_to_service) do + if game.entity_prototypes[name] ~= nil then + name_to_service[name] = service + end + end + for _, prot in pairs(game.entity_prototypes) do if prot.has_flag("hidden") or not prot.has_flag("player-creation") then -- not adding it @@ -1788,20 +1832,34 @@ function M.scan_prototypes() end -- called once at startup if scan_prototypes() returns something different -function M.scan_surfaces() - clog("[%s] item-network: Scanning surfaces", game.tick) +function M.scan_surfaces(surface_index) local name_filter = {} for name, _ in pairs(global.name_service_map) do table.insert(name_filter, name) end - for _, surface in pairs(game.surfaces) do - local entities = surface.find_entities_filtered { name = name_filter } - for _, ent in ipairs(entities) do - M.entity_info_add(ent) + + local function scan_surface(surface) + if surface ~= nil then + log(("Scanning surface [%s] %s"):format(surface.index, surface.name)) + local entities = surface.find_entities_filtered { name = name_filter } + for _, ent in ipairs(entities) do + --log((" - ent [%s] @ %s"):format(ent.name, serpent.line(ent.position))) + M.entity_info_add(ent) + end + end + end + + if surface_index == nil then + for _, surface in pairs(game.surfaces) do + scan_surface(surface) end + --M.reset_queues() + else + -- scanning a single new surface + scan_surface(game.surfaces[surface_index]) end - M.reset_queues() - clog("[%s] item-network: Scanning complete", game.tick) + + log("Scanning complete") end --[[ @@ -1838,7 +1896,7 @@ end -- not sure this belongs here... Event.on_configuration_changed(function () - clog("item-network: *** CONFIGURATION CHANGED ***") + log("*** CONFIGURATION CHANGED ***") -- need to rescan the fuel table global.ammo_table = nil global.fuel_table = nil @@ -1848,10 +1906,20 @@ end) Event.register( defines.events.on_runtime_mod_setting_changed, function(event) - clog("item-network: *** SETTINGS CHANGED *** %s", serpent.line(event)) + log(("item-network: *** SETTINGS CHANGED *** %s"): format(serpent.line(event))) M.reread_settings() end) +local function log_event(event) + log(("event: %s"):format(serpent.line(event))) + if event.name == defines.events.on_surface_created then + global.need_to_scan = true + end +end + +Event.register(defines.events.on_surface_imported, log_event) +Event.register(defines.events.on_surface_created, log_event) + -- need to run setup() as soon as 'game' is available Event.on_init(M.setup) Event.on_nth_tick(1, function() M.queue_service() end) diff --git a/src/NetworkChest.lua b/src/NetworkChest.lua index d83f8a0..ce82eeb 100644 --- a/src/NetworkChest.lua +++ b/src/NetworkChest.lua @@ -32,11 +32,9 @@ local function generic_create_handler(event) return end - --clog("generic_create_handler [%s] => %s", entity.name, serpent.line(svc_func)) + log(("create [%s] [%s] [%s] => %s"):format(entity.name, entity.type, entity.unit_number, service_type)) if type(svc_func.create) == "function" then svc_func.create(entity, event.tags) - else - --clog("ERROR: no create for %s", entity.name) end end diff --git a/src/Paths.lua b/src/Paths.lua index 28d2acd..62f1c95 100644 --- a/src/Paths.lua +++ b/src/Paths.lua @@ -1,5 +1,5 @@ local Paths = { - graphics = "__item-network__/graphics", + graphics = "__cheat-network__/graphics", } return Paths diff --git a/src/Util.lua b/src/Util.lua new file mode 100644 index 0000000..329b475 --- /dev/null +++ b/src/Util.lua @@ -0,0 +1,221 @@ +local Util = {} + +--- Clamps a value to be in the range [low, high] +---@param val number The value to clamp +---@param low number The minimum value +---@param high number The maximum value +---@return number n The clamped value +function Util.clamp(val, low, high) + return math.min(high, math.max(val, low)) +end + +--- Computes the weighted average between a and b +---@param a number The first value +---@param a_weight number The weight of the first value +---@param b number The second value +---@param b_weight number The weight of the second value +---@return number n The weighted average +function Util.weighted_average(a, a_weight, b, b_weight) + local total_weight = a_weight + b_weight + if total_weight == 0 then + total_weight = 1 + end + a_weight = a_weight / total_weight + b_weight = b_weight / total_weight + return a * a_weight + b * b_weight +end + +--- Adds the new keys from src into dest +---@param dest table The table to add keys to, this table will be modified +---@param src table The table to get keys from +---@return table dest The destination table +function Util.dictionary_merge(dest, src) + for k, v in pairs(src) do + if dest[k] == nil then + dest[k] = v + end + end + return dest +end + +--- Merges two tables, overwrites existing keys in dest +---@param dest table The table to add keys to, this table will be modified +---@param src table The table to get keys from +---@return table dest The destination table +function Util.table_merge(dest, src) + for k, v in pairs(src) do + dest[k] = v + end + return dest +end + +--- Returns the keys from dict as an array +---@param dict table The table to get keys from +---@return table keys The array of keys from dict +function Util.table_keys(dict) + local keys = {} + for k, v in pairs(dict) do + table.insert(keys, k) + end + return keys +end + +--- Filters the given table using the given filter function +---@param t any The table to filter +---@param filter_fn function A filter function that takes the arguments (k, v), +---return a truthy value to include it in the filtered table +---@return table filtered The filtered result +function Util.table_filter(t, filter_fn) + local ret = {} + for k, v in pairs(t) do + if filter_fn(k, v) then + ret[k] = v + end + end + return ret +end + +--- Gets the min, max and sum of all the values in a table +---@param dict table The table to get values from +---@return number num_vals The number of values in the table +---@return number sum The sum of all the values +---@return number min The minimum of all the values +---@return number max The maximum of all the values +function Util.table_val_stats(dict) + local num_vals = 0 + local sum = 0 + local min = math.huge + local max = -math.huge + for k, v in pairs(dict) do + num_vals = num_vals + 1 + sum = sum + v + min = math.min(min, v) + max = math.max(max, v) + end + return num_vals, sum, min, max +end + +--- Gets the sum of all the values in a table +---@param dict table The table to get values from +---@return number sum The sum of all the values +function Util.table_sum_vals(dict) + local sum = 0 + for k, v in pairs(dict) do + sum = sum + v + end + return sum +end + +--- Gets the minimum value from a table +---@param dict table The table to get values from +---@return number minimum The minimum value +function Util.table_min_val(dict) + local min = math.huge + for k, v in pairs(dict) do + min = math.min(min, v) + end + return min +end + +--- Extends the dest array with the contents of the src array +---@param dest table The destination array (will be modified) +---@param src table The source array +---@return table dest The destination array +function Util.array_extend(dest, src) + local insert = table.insert + for _, v in ipairs(src) do + insert(dest, v) + end + return dest +end + +--- Gets the next key+value pair from t that was fetched at least min_tick_diff ticks ago +---@param state_key string The key to use when storing the timing state in global +---@param min_tick_diff integer Minimum number of ticks for an item to be considered out of date +---@param t table The table of values to check +---@return any k The first key that needs updating +---@return any v The corresponding value of the key that needs updating +function Util.get_next_updatable(state_key, min_tick_diff, t) + local last_table_key = global[state_key .. "_last_key"] + local last_update_key = state_key .. "_last_update" + if global[last_update_key] == nil then + global[last_update_key] = {} + end + + local last_updates = global[last_update_key] + if t[last_table_key] == nil or next(t, last_table_key) == nil then + last_table_key = nil + end + + for k, v in next, t, last_table_key do + if game.tick - (last_updates[k] or 0) >= min_tick_diff then + last_updates[k] = game.tick + global[state_key .. "_last_key"] = k + return k, v + end + end +end + +--- Iterates fluidboxes that are of a certain production_type +---@param entity LuaEntity The entity with fluidboxes to iterate over +---@param prod_type_pattern string The pattern to check against the production_type of each fluidbox +---@param iter_all boolean Set to true to iterate all fluidboxes instead of only those containing a fluid +---@return function iterator An iterator over the matched fluidboxes +function Util.iter_fluidboxes(entity, prod_type_pattern, iter_all) + local i = 0 + local n = #entity.fluidbox + return function() + while i < n do + i = i + 1 + local fluid = entity.fluidbox[i] + local proto = entity.fluidbox.get_prototype(i) + if (iter_all or fluid ~= nil) and string.match(proto.production_type or "", prod_type_pattern) then + local filter = entity.fluidbox.get_filter(i) + if not fluid and filter then + fluid = { + name = filter.name, + temperature = game.fluid_prototypes[filter.name].default_temperature, + amount = 0 + } + end + return i, fluid, filter, proto + end + end + end +end + +--- Gets the default temperature for given fluid +---@param fluid_name string The name of the fluid +---@return number temperature The default temperature for the fluid +function Util.get_default_fluid_temperature(fluid_name) + local temp = game.fluid_prototypes[fluid_name].default_temperature + assert(temp ~= nil) + return temp +end + +--- Returns a function for sorting prototypes by their .order attributes +--- Items are ordered by their groups, subgroups, then finally by their order +---@param get_prototype_fn function function that returns the prototype for the given item +---@param order_comp_fn function|nil function that is used to compare the two order strings, defaults to a < b +---@return function comp comparison function to be used with table.sort +function Util.prototype_order_comp_fn(get_prototype_fn, order_comp_fn) + order_comp_fn = order_comp_fn or function(a, b) + return a < b + end + return function(a, b) + local proto_a = get_prototype_fn(a) + local proto_b = get_prototype_fn(b) + if proto_a.group.name ~= proto_b.group.name then + return order_comp_fn(proto_a.group.order, proto_b.group.order) + end + if proto_a.subgroup.name ~= proto_b.subgroup.name then + return order_comp_fn(proto_a.subgroup.order, proto_b.subgroup.order) + end + if proto_a.order ~= proto_b.order then + return order_comp_fn(proto_a.order, proto_b.order) + end + return order_comp_fn(proto_a.name, proto_b.name) + end +end + +return Util diff --git a/src/log_all_events.lua b/src/log_all_events.lua new file mode 100644 index 0000000..40b3a6d --- /dev/null +++ b/src/log_all_events.lua @@ -0,0 +1,194 @@ +local Event = require('__stdlib__/stdlib/event/event') + +local function log_event(event) + log(("event: %s"):format(serpent.line(event))) + local entity = event.created_entity or event.entity or event.destination + if entity ~= nil then + log(("entity: [%s] %s %s @ %s"):format( + entity.unit_number, entity.name, entity.type, serpent.line(entity.position))) + end +end + +Event.register(defines.events.on_ai_command_completed, log_event) +Event.register(defines.events.on_area_cloned, log_event) +Event.register(defines.events.on_biter_base_built, log_event) +Event.register(defines.events.on_brush_cloned, log_event) +Event.register(defines.events.on_build_base_arrived, log_event) +Event.register(defines.events.on_built_entity, log_event) +Event.register(defines.events.on_cancelled_deconstruction, log_event) +Event.register(defines.events.on_cancelled_upgrade, log_event) +Event.register(defines.events.on_character_corpse_expired, log_event) +Event.register(defines.events.on_chart_tag_added, log_event) +Event.register(defines.events.on_chart_tag_modified, log_event) +Event.register(defines.events.on_chart_tag_removed, log_event) +Event.register(defines.events.on_chunk_charted, log_event) +Event.register(defines.events.on_chunk_deleted, log_event) +Event.register(defines.events.on_chunk_generated, log_event) +Event.register(defines.events.on_combat_robot_expired, log_event) +Event.register(defines.events.on_console_chat, log_event) +Event.register(defines.events.on_console_command, log_event) +Event.register(defines.events.on_cutscene_cancelled, log_event) +Event.register(defines.events.on_cutscene_finished, log_event) +Event.register(defines.events.on_cutscene_started, log_event) +Event.register(defines.events.on_cutscene_waypoint_reached, log_event) +Event.register(defines.events.on_difficulty_settings_changed, log_event) +Event.register(defines.events.on_entity_cloned, log_event) +Event.register(defines.events.on_entity_color_changed, log_event) +Event.register(defines.events.on_entity_damaged, log_event) +Event.register(defines.events.on_entity_destroyed, log_event) +Event.register(defines.events.on_entity_died, log_event) +Event.register(defines.events.on_entity_logistic_slot_changed, log_event) +Event.register(defines.events.on_entity_renamed, log_event) +Event.register(defines.events.on_entity_settings_pasted, log_event) +Event.register(defines.events.on_entity_spawned, log_event) +Event.register(defines.events.on_equipment_inserted, log_event) +Event.register(defines.events.on_equipment_removed, log_event) +Event.register(defines.events.on_force_cease_fire_changed, log_event) +Event.register(defines.events.on_force_created, log_event) +Event.register(defines.events.on_force_friends_changed, log_event) +Event.register(defines.events.on_force_reset, log_event) +Event.register(defines.events.on_forces_merged, log_event) +Event.register(defines.events.on_forces_merging, log_event) +Event.register(defines.events.on_game_created_from_scenario, log_event) +Event.register(defines.events.on_gui_checked_state_changed, log_event) +Event.register(defines.events.on_gui_click, log_event) +Event.register(defines.events.on_gui_closed, log_event) +Event.register(defines.events.on_gui_confirmed, log_event) +Event.register(defines.events.on_gui_elem_changed, log_event) +Event.register(defines.events.on_gui_hover, log_event) +Event.register(defines.events.on_gui_leave, log_event) +Event.register(defines.events.on_gui_location_changed, log_event) +Event.register(defines.events.on_gui_opened, log_event) +Event.register(defines.events.on_gui_selected_tab_changed, log_event) +Event.register(defines.events.on_gui_selection_state_changed, log_event) +Event.register(defines.events.on_gui_switch_state_changed, log_event) +Event.register(defines.events.on_gui_text_changed, log_event) +Event.register(defines.events.on_gui_value_changed, log_event) +Event.register(defines.events.on_land_mine_armed, log_event) +Event.register(defines.events.on_lua_shortcut, log_event) +Event.register(defines.events.on_marked_for_deconstruction, log_event) +Event.register(defines.events.on_marked_for_upgrade, log_event) +Event.register(defines.events.on_market_item_purchased, log_event) +Event.register(defines.events.on_mod_item_opened, log_event) +Event.register(defines.events.on_permission_group_added, log_event) +Event.register(defines.events.on_permission_group_deleted, log_event) +Event.register(defines.events.on_permission_group_edited, log_event) +Event.register(defines.events.on_permission_string_imported, log_event) +Event.register(defines.events.on_picked_up_item, log_event) +Event.register(defines.events.on_player_alt_reverse_selected_area, log_event) +Event.register(defines.events.on_player_alt_selected_area, log_event) +Event.register(defines.events.on_player_ammo_inventory_changed, log_event) +Event.register(defines.events.on_player_armor_inventory_changed, log_event) +Event.register(defines.events.on_player_banned, log_event) +Event.register(defines.events.on_player_built_tile, log_event) +Event.register(defines.events.on_player_cancelled_crafting, log_event) +Event.register(defines.events.on_player_changed_force, log_event) +Event.register(defines.events.on_player_changed_position, log_event) +Event.register(defines.events.on_player_changed_surface, log_event) +Event.register(defines.events.on_player_cheat_mode_disabled, log_event) +Event.register(defines.events.on_player_cheat_mode_enabled, log_event) +Event.register(defines.events.on_player_clicked_gps_tag, log_event) +Event.register(defines.events.on_player_configured_blueprint, log_event) +Event.register(defines.events.on_player_configured_spider_remote, log_event) +Event.register(defines.events.on_player_crafted_item, log_event) +Event.register(defines.events.on_player_created, log_event) +Event.register(defines.events.on_player_cursor_stack_changed, log_event) +Event.register(defines.events.on_player_deconstructed_area, log_event) +Event.register(defines.events.on_player_demoted, log_event) +Event.register(defines.events.on_player_died, log_event) +Event.register(defines.events.on_player_display_resolution_changed, log_event) +Event.register(defines.events.on_player_display_scale_changed, log_event) +Event.register(defines.events.on_player_driving_changed_state, log_event) +Event.register(defines.events.on_player_dropped_item, log_event) +Event.register(defines.events.on_player_fast_transferred, log_event) +Event.register(defines.events.on_player_flushed_fluid, log_event) +Event.register(defines.events.on_player_gun_inventory_changed, log_event) +Event.register(defines.events.on_player_input_method_changed, log_event) +Event.register(defines.events.on_player_joined_game, log_event) +Event.register(defines.events.on_player_kicked, log_event) +Event.register(defines.events.on_player_left_game, log_event) +Event.register(defines.events.on_player_main_inventory_changed, log_event) +Event.register(defines.events.on_player_mined_entity, log_event) +Event.register(defines.events.on_player_mined_item, log_event) +Event.register(defines.events.on_player_mined_tile, log_event) +Event.register(defines.events.on_player_muted, log_event) +Event.register(defines.events.on_player_pipette, log_event) +Event.register(defines.events.on_player_placed_equipment, log_event) +Event.register(defines.events.on_player_promoted, log_event) +Event.register(defines.events.on_player_removed, log_event) +Event.register(defines.events.on_player_removed_equipment, log_event) +Event.register(defines.events.on_player_repaired_entity, log_event) +Event.register(defines.events.on_player_respawned, log_event) +Event.register(defines.events.on_player_reverse_selected_area, log_event) +Event.register(defines.events.on_player_rotated_entity, log_event) +Event.register(defines.events.on_player_selected_area, log_event) +Event.register(defines.events.on_player_set_quick_bar_slot, log_event) +Event.register(defines.events.on_player_setup_blueprint, log_event) +Event.register(defines.events.on_player_toggled_alt_mode, log_event) +Event.register(defines.events.on_player_toggled_map_editor, log_event) +Event.register(defines.events.on_player_trash_inventory_changed, log_event) +Event.register(defines.events.on_player_unbanned, log_event) +Event.register(defines.events.on_player_unmuted, log_event) +Event.register(defines.events.on_player_used_capsule, log_event) +Event.register(defines.events.on_player_used_spider_remote, log_event) +Event.register(defines.events.on_post_entity_died, log_event) +Event.register(defines.events.on_pre_build, log_event) +Event.register(defines.events.on_pre_chunk_deleted, log_event) +Event.register(defines.events.on_pre_entity_settings_pasted, log_event) +Event.register(defines.events.on_pre_ghost_deconstructed, log_event) +Event.register(defines.events.on_pre_ghost_upgraded, log_event) +Event.register(defines.events.on_pre_permission_group_deleted, log_event) +Event.register(defines.events.on_pre_permission_string_imported, log_event) +Event.register(defines.events.on_pre_player_crafted_item, log_event) +Event.register(defines.events.on_pre_player_died, log_event) +Event.register(defines.events.on_pre_player_left_game, log_event) +Event.register(defines.events.on_pre_player_mined_item, log_event) +Event.register(defines.events.on_pre_player_removed, log_event) +Event.register(defines.events.on_pre_player_toggled_map_editor, log_event) +Event.register(defines.events.on_pre_robot_exploded_cliff, log_event) +Event.register(defines.events.on_pre_script_inventory_resized, log_event) +Event.register(defines.events.on_pre_surface_cleared, log_event) +Event.register(defines.events.on_pre_surface_deleted, log_event) +Event.register(defines.events.on_research_cancelled, log_event) +Event.register(defines.events.on_research_finished, log_event) +Event.register(defines.events.on_research_reversed, log_event) +Event.register(defines.events.on_research_started, log_event) +Event.register(defines.events.on_resource_depleted, log_event) +Event.register(defines.events.on_robot_built_entity, log_event) +Event.register(defines.events.on_robot_built_tile, log_event) +Event.register(defines.events.on_robot_exploded_cliff, log_event) +Event.register(defines.events.on_robot_mined, log_event) +Event.register(defines.events.on_robot_mined_entity, log_event) +Event.register(defines.events.on_robot_mined_tile, log_event) +Event.register(defines.events.on_robot_pre_mined, log_event) +Event.register(defines.events.on_rocket_launch_ordered, log_event) +Event.register(defines.events.on_rocket_launched, log_event) +Event.register(defines.events.on_runtime_mod_setting_changed, log_event) +Event.register(defines.events.on_script_inventory_resized, log_event) +--Event.register(defines.events.on_script_path_request_finished, log_event) +Event.register(defines.events.on_script_trigger_effect, log_event) +Event.register(defines.events.on_sector_scanned, log_event) +Event.register(defines.events.on_selected_entity_changed, log_event) +Event.register(defines.events.on_spider_command_completed, log_event) +--Event.register(defines.events.on_string_translated, log_event) +Event.register(defines.events.on_surface_cleared, log_event) +Event.register(defines.events.on_surface_created, log_event) +Event.register(defines.events.on_surface_deleted, log_event) +Event.register(defines.events.on_surface_imported, log_event) +Event.register(defines.events.on_surface_renamed, log_event) +Event.register(defines.events.on_technology_effects_reset, log_event) +Event.register(defines.events.on_train_changed_state, log_event) +Event.register(defines.events.on_train_created, log_event) +Event.register(defines.events.on_train_schedule_changed, log_event) +Event.register(defines.events.on_trigger_created_entity, log_event) +Event.register(defines.events.on_trigger_fired_artillery, log_event) +Event.register(defines.events.on_unit_added_to_group, log_event) +Event.register(defines.events.on_unit_group_created, log_event) +Event.register(defines.events.on_unit_group_finished_gathering, log_event) +Event.register(defines.events.on_unit_removed_from_group, log_event) +Event.register(defines.events.on_worker_robot_expired, log_event) +Event.register(defines.events.script_raised_built, log_event) +Event.register(defines.events.script_raised_destroy, log_event) +Event.register(defines.events.script_raised_revive, log_event) +Event.register(defines.events.script_raised_set_tiles, log_event) +Event.register(defines.events.script_raised_teleported, log_event) diff --git a/src/my_entity_gui.lua b/src/my_entity_gui.lua new file mode 100644 index 0000000..8722823 --- /dev/null +++ b/src/my_entity_gui.lua @@ -0,0 +1,557 @@ +--[[ +Test code to put up a Entity GUI next to serviced items. +]] +local GlobalState = require "src.GlobalState" +local GUIDispatcher = require 'src.GUIDispatcher' +local GUICommon = require 'src.GUICommon' +local service_furnace = require 'src.service_furnace' +local GUIComponentSliderInput = require "src.GUIComponentSliderInput" + +local M = {} + +local EntityTypeGUIAnchors = { + ["assembling-machine"] = defines.relative_gui_type.assembling_machine_gui, + ["car"] = defines.relative_gui_type.car_gui, + ["logistic-container"] = defines.relative_gui_type.container_gui, + ["container"] = defines.relative_gui_type.container_gui, + ["furnace"] = defines.relative_gui_type.furnace_gui, + ["lab"] = defines.relative_gui_type.lab_gui, + ["mining-drill"] = defines.relative_gui_type.mining_drill_gui, + ["boiler"] = defines.relative_gui_type.entity_with_energy_source_gui, + ["burner-generator"] = defines.relative_gui_type.entity_with_energy_source_gui, + ["artillery-turret"] = defines.relative_gui_type.container_gui, + ["ammo-turret"] = defines.relative_gui_type.container_gui, + ["reactor"] = defines.relative_gui_type.reactor_gui, + ["storage-tank"] = defines.relative_gui_type.storage_tank_gui, + ["rocket-silo"] = defines.relative_gui_type.rocket_silo_gui, + ["spider-vehicle"] = defines.relative_gui_type.spider_vehicle_gui, + ["constant-combinator"] = defines.relative_gui_type.constant_combinator_gui, +} + +function M.initialize() + if global.entity_panel_location == nil then + global.entity_panel_location = {} + end + if global.entity_panel_pending_relocations == nil then + global.entity_panel_pending_relocations = {} + end +end + +M.GUI_ENTITY_PANEL ="cin-entity-panel" +M.GUI_CLOSE_EVENT = "cin-entity-panel-close" +local DISABLED_CHECKED_EVENT = "cin-entity-panel-disable" +local PRIORITISE_CHECKED_EVENT = "cin-entity-panel-prioritise" +local CONDITION_ITEM_EVENT = "cin-entity-panel-condition-item" +M.OPERATIONS = { "≥", "≤" } +local CONDITION_OP_EVENT = "cin-entity-panel-condition-op" +local CONDITION_VALUE_BUTTON_EVENT = "cin-entity-panel-condition-button" +local CONDITION_VALUE_CHANGED_EVENT = "cin-entity-panel-condition-value-changed" +local CONDITION_SURFACE_CHANGED_EVENT = "cin-entity-panel-condition-surface-changed" +local CONDITION_SURFACE_RESET_EVENT = "cin-entity-panel-condition-surface-reset" +local FURNACE_RECIPE_EVENT = "cin-entity-panel-furnace-recipe" +local RETURN_EXCESS_CHECKED_EVENT = "cin-entity-panel-return-excess" +local SHOW_PRIORITY_GUI_EVENT = "cin-entity-panel-show-priority-gui" + +-- get some sort of unique key for the entity (generic) +local function get_location_key(player, entity_name) + return ("%d;%s;%s;%s"):format( + player.index, + player.opened.get_mod(), + entity_name, + player.opened.name + ) +end + +------------------------------------------------------------------------------- + +--[[ +Look for and close the GUI_ENTITY_PANEL. +If it was under player.gui.screen, then return the window location. +]] +function M.gui_close(player) + local last_position + local window = player.gui.relative[M.GUI_ENTITY_PANEL] + if window then + window.destroy() + end + + window = player.gui.screen[M.GUI_ENTITY_PANEL] + if window then + last_position = window.location + window.destroy() + end + return last_position +end + +local function add_panel_frame(parent, caption, tooltip) + parent.add({ + type = "line", + style = "control_behavior_window_line" + }) + local frame = parent.add({ + type = "frame", + style = "invisible_frame", + direction = "vertical" + }) + local label = frame.add({ + type = "label", + style = "heading_2_label", + caption = caption, + tooltip = tooltip + }) + label.style.padding = { 4, 0, 4, 0 } + return frame +end + +--[[ +Build the GUI content. +]] +local function add_gui_content(window, entity, info) + local data_id = entity.unit_number + local config = info.config or {} + + print(serpent.block(info)) + + local frame = window.add({ + type = "frame", + style = "inside_shallow_frame_with_padding", + direction = "vertical" + }) + + -- Disabled + frame.add({ + type = "checkbox", + caption = "Service Disabled [img=info]", + tooltip = "Disable all servicing of this entity", + state = (config.disabled == true), + tags = { id = data_id, event = DISABLED_CHECKED_EVENT } + }) + + -- Prioritise + frame.add({ + type = "checkbox", + caption = "Prioritise [img=info]", + tooltip = "Allow consumption of reserved resources", + state = (config.use_reserved == true), + tags = { id = data_id, event = PRIORITISE_CHECKED_EVENT } + }) + frame.add({ + type = "line", + style = "control_behavior_window_line" + }) + + -- Condition + if not config.condition then + config.condition = {} + end + local condition = config.condition + local condition_value = config.condition_value or 0 + + local condition_frame = frame.add({ + type = "frame", + name = "condition_frame", + style = "invisible_frame_with_title", + caption = { "gui-control-behavior-modes-guis.enabled-condition" }, + direction = "vertical" + }) + local condition_controls_flow = condition_frame.add({ + type = "flow", + name = "condition_controls_flow", + direction = "horizontal" + }) + condition_controls_flow.style.vertical_align = "center" + + local fluid_name = "" -- Storage.unpack_fluid_item_name(condition.item or "") + condition_controls_flow.add({ + type = "choose-elem-button", + elem_type = "signal", + style = "slot_button_in_shallow_frame", + signal = { + type = fluid_name and "fluid" or "item", + name = fluid_name or condition.item + }, + tags = { id = data_id, event = CONDITION_ITEM_EVENT } + }) + condition_controls_flow.add({ + type = "drop-down", + items = M.OPERATIONS, + selected_index = config.condition_op or 1, + style = "circuit_condition_comparator_dropdown", + tags = { id = data_id, event = CONDITION_OP_EVENT } + }) + local condition_value_btn = condition_controls_flow.add({ + type = "button", + name = "condition_value", + style = "slot_button_in_shallow_frame", + caption = condition_value .. "%", + tags = { event = CONDITION_VALUE_BUTTON_EVENT } + }) + condition_value_btn.style.font_color = { 1, 1, 1 } + + local condition_slider_flow = condition_frame.add({ + type = "flow", + name = "slider_flow", + style = "player_input_horizontal_flow", + }) + condition_slider_flow.style.top_padding = 4 + condition_slider_flow.visible = false + GUIComponentSliderInput.create( + condition_slider_flow, + { + value = condition_value, + maximum_value = 100, + style = "slider", + tags = { id = data_id, event = { [CONDITION_VALUE_CHANGED_EVENT] = true } } + }, + { + allow_negative = false, + style = "very_short_number_textfield", + tags = { id = data_id, event = { [CONDITION_VALUE_CHANGED_EVENT] = true } } + } + ) + condition_slider_flow.slider.style.width = 100 + + condition_slider_flow.add({ + type = "label", + caption = "%", + }) +--[[ + local surface_names = Util.table_keys(game.surfaces) + local current_surface = condition.surface or entity.surface.name + local current_surface_i = flib_table.find(surface_names, current_surface) + if current_surface_i == nil then + table.insert(surface_names, ("%s%s (not found)%s"):format(R.COLOUR_RED, current_surface, R.COLOUR_END)) + current_surface_i = #surface_names + end + if table_size(surface_names) > 1 then + local condition_surface_flow = condition_frame.add({ + type = "flow", + direction = "horizontal" + }) + condition_surface_flow.style.vertical_align = "center" + + condition_surface_flow.add({ + type = "label", + style = "heading_2_label", + caption = "Storage source [img=info]", + tooltip = "The surface of the storage to use when checking the condition, and reading signals (for [entity=arr-combinator] Auto Resource Combinators)" + }) + + local button = condition_surface_flow.add({ + type = "sprite-button", + resize_to_sprite = false, + sprite = "utility/reset_white", + tooltip = "Click to reset to current surface", + tags = { id = data_id, event = CONDITION_SURFACE_RESET_EVENT } + }) + button.style.size = { 24, 24 } + + condition_frame.add({ + type = "drop-down", + name = "condition_surface", + items = surface_names, + selected_index = current_surface_i, + tags = { id = data_id, event = CONDITION_SURFACE_CHANGED_EVENT } + }) + end + + if entity.name == "arr-logistic-requester-chest" then + local sub_frame = add_panel_frame(frame, "Requester Chest") + frame.add({ + type = "checkbox", + caption = "Return excess items [img=info]", + tooltip = "Return items that are not requested or are above the requested amount", + state = (data.return_excess == true), + tags = { id = data_id, event = RETURN_EXCESS_CHECKED_EVENT } + }) + end +]] + if entity.type == "furnace" then + local sub_frame = add_panel_frame( + frame, + { "", { "description.recipe" }, " [img=info]" }, + "The new recipe will be applied on the next production cycle, when the productivity bar is empty." + ) + local current_recipe = service_furnace.get_recipe_name(info) + local filters = {} + for category, _ in pairs(entity.prototype.crafting_categories) do + table.insert(filters, { filter = "category", category = category }) + end + --FIXME: would be great to look up if the recipe has been researched + sub_frame.add({ + type = "choose-elem-button", + elem_type = "recipe", + style = "slot_button_in_shallow_frame", + recipe = current_recipe, + elem_filters = filters, + tags = { id = data_id, event = FURNACE_RECIPE_EVENT } + }) + end +--[[ + local priority_sets = ItemPriorityManager.get_priority_sets_for_entity(entity) + -- { [group] = { set1_key, set2_key, ... } } + local related_priority_set_keys = {} + for set_key, priority_set in pairs(priority_sets) do + if priority_set.group then + local sets = related_priority_set_keys[priority_set.group] or {} + table.insert(sets, set_key) + related_priority_set_keys[priority_set.group] = sets + end + end + if table_size(related_priority_set_keys) > 0 then + local sub_frame = add_panel_frame( + frame, + "Item Priority [img=info]", + { + "", ("Effects every [entity=%s] "):format(entity.name), entity.localised_name, + "\nItems are used from left to right.\n", + R.HINT, { "control-keys.mouse-button-1" }, R.HINT_END, " to set item quantity.", + } + ) + sub_frame.style.vertically_stretchable = false + local inner_flow = sub_frame.add({ + type = "flow", + direction = "vertical" + }) + inner_flow.style.left_margin = 4 + inner_flow.style.vertical_spacing = 0 + for group, set_keys in pairs(related_priority_set_keys) do + local label_flow = inner_flow.add({ + type = "flow", + direction = "horizontal" + }) + label_flow.style.vertical_align = "center" + + local label = label_flow.add({ + type = "label", + style = "heading_2_label", + caption = group, + }) + label.style.bottom_padding = 0 + + local button = label_flow.add({ + type = "sprite-button", + resize_to_sprite = false, + sprite = "arr-logo", + tooltip = "Click to show in the full priority list window", + tags = { + event = SHOW_PRIORITY_GUI_EVENT, + group = group, + entity = entity.name, + } + }) + button.style.size = { 24, 24 } + + for _, set_key in ipairs(set_keys) do + local flow = inner_flow.add({ + type = "flow", + }) + GUIComponentItemPrioritySet.create(flow, priority_sets, set_key, 6) + end + end + end +]] +end + + +local function my_on_gui_opened(event) + if global.test_gui ~= nil then + global.test_gui.destroy() + global.test_gui = nil + end + local player = game.players[event.player_index] + if player == nil then + print(string.format("on_gui_open: typ=%s tis nil", event.gui_type)) + return + end + if player.gui == nil then + print(string.format("on_gui_open: typ=%s gui is nil", event.gui_type)) + return + end + local entity = event.entity + if event.gui_type == defines.gui_type.entity and entity ~= nil and entity.type == "furnace" then + print(string.format("on_gui_open: type=%s name=%s unum=%s", event.gui_type, entity.name, entity.unit_number)) + global.test_gui = player.gui.relative.add({ + type = "frame", + style = "inset_frame_container_frame", + anchor = { + gui = defines.relative_gui_type.furnace_gui, + position = defines.relative_gui_position.right, + }, + }) + local vflow = global.test_gui.add({ + type = "flow", + direction = "vertical", + }) + local hflow = vflow.add({ + type = "flow", + direction = "horizontal", + }) + + hflow.add({ + type = "label", + caption = "Pick The Recipe", + style = "frame_title", + }) + vflow.add({ + type = "sprite-button", + sprite = "item/locomotive", + }) + -- the category should be taken from the furnace categories + local ef = {} + local mode + for cc, _ in pairs(entity.prototype.crafting_categories) do + table.insert(ef, { mode = mode, filter = "category", category = cc }) + mode = "or" + end + table.insert(ef, { mode = "and", filter="has-product-item" }) + table.insert(ef, { mode = "and", filter="hidden", invert=true }) + vflow.add({ + type = "choose-elem-button", + elem_type = "recipe", + elem_filters = ef, + }) + end +end + +local function on_gui_opened(event, tags, player) + local last_position = M.gui_close(player) + local entity = event.entity + if entity == nil or not entity.valid or entity.unit_number == nil then + return + end + local info = GlobalState.entity_info_get(entity.unit_number) + if info == nil then + -- see if this is something we handle + if GlobalState.get_service_type_for_entity(entity.name) == nil then + return + end + info = GlobalState.entity_info_add(entity) + end + + -- if .opened is a custom UI, then open on screen instead because we can't anchor + if player.opened and player.opened.object_name == "LuaGuiElement" then + local parent = player.gui.screen + local window = parent.add({ + type = "frame", + name = M.GUI_ENTITY_PANEL, + direction = "vertical", + style = "inner_frame_in_outer_frame", + tags = { entity_name = entity.name, unit_number = entity.unit_number } + }) + + GUICommon.create_header(window, "Cheat Network", M.GUI_CLOSE_EVENT) + -- use the location from the previous time a similar UI was opened + -- this might be incorrect, but should be corrected on the next tick + local location_key = get_location_key(player, entity.name) + last_position = global.entity_panel_location[location_key] or last_position + local res = player.display_resolution + if last_position and last_position.x + 100 < res.width and last_position.y + 100 < res.height then + window.location = last_position + else + window.force_auto_center() + end + -- the position of the parent GUI will only be known on the next tick + -- so set a flag for on_tick to reposition us later + global.entity_panel_pending_relocations[player.index] = { + player = player, + tick = game.tick + 1 + } + add_gui_content(window, entity, info) + return + end + + local anchor = EntityTypeGUIAnchors[entity.type] + if not anchor then + log(("FIXME: don't know how to anchor to entity GUI name=%s type=%s"):format(entity.name, entity.type)) + return + end + + local relative = player.gui.relative + local window = relative[GUICommon.GUI_ENTITY_PANEL] + window = relative.add({ + type = "frame", + name = GUICommon.GUI_ENTITY_PANEL, + direction = "vertical", + style = "inner_frame_in_outer_frame", + anchor = { + position = defines.relative_gui_position.right, + gui = anchor, + }, + caption = "Cheat Network", + tags = { entity_id = entity.unit_number } + }) + add_gui_content(window, entity, info) +end + +local function on_gui_closed(event, tags, player) + M.gui_close(player) +end + +local function on_disabled_checked(event, tags, player) + GlobalState.entity_info_config_update(tags.id, { disabled = event.element.state }) +end + +local function on_prioritise_checked(event, tags, player) + GlobalState.entity_info_config_update(tags.id, { use_reserved = event.element.state }) +end + +local function on_condition_item_changed(event, tags, player) + --[[ + local signal = event.element.elem_value + local storage_key = signal and (signal.type == "fluid" and Storage.get_fluid_storage_key(signal.name) or signal.name) + if signal and signal.type == "virtual" then + event.element.elem_value = nil + return + end + global.entity_data[tags.id].condition.item = storage_key + GlobalState.entity_info_config_update(tags.id, { condition_item = storage_key }) + ]] +end + +local function on_condition_op_changed(event, tags, player) + GlobalState.entity_info_config_update(tags.id, { condition_op = event.element.selected_index }) +end + +local function on_furnace_recipe_changed(event, tags, player) + local new_recipe_name = event.element.elem_value + local info = GlobalState.entity_info_get(tags.id) + if not info then + return + end + --local entity = global.entities[tags.id] + -- go back to the current recipe + if not new_recipe_name then + --local recipe = info.config.recipe -- FurnaceRecipeManager.get_recipe(entity) + event.element.elem_value = service_furnace.get_recipe_name(info) + return + end + + service_furnace.set_recipe_name(info, new_recipe_name) + --FurnaceRecipeManager.set_recipe(entity, new_recipe_name) +end + +GUIDispatcher.register(defines.events.on_gui_elem_changed, FURNACE_RECIPE_EVENT, on_furnace_recipe_changed) + +GUIDispatcher.register(defines.events.on_gui_opened, nil, on_gui_opened) +GUIDispatcher.register(defines.events.on_gui_closed, nil, on_gui_closed) + +GUIDispatcher.register(defines.events.on_gui_checked_state_changed, DISABLED_CHECKED_EVENT, on_disabled_checked) +GUIDispatcher.register(defines.events.on_gui_checked_state_changed, PRIORITISE_CHECKED_EVENT, on_prioritise_checked) + +GUIDispatcher.register(defines.events.on_gui_elem_changed, CONDITION_ITEM_EVENT, on_condition_item_changed) +GUIDispatcher.register(defines.events.on_gui_selection_state_changed, CONDITION_OP_EVENT, on_condition_op_changed) +GUIDispatcher.register(defines.events.on_gui_click, CONDITION_VALUE_BUTTON_EVENT, on_condition_value_clicked) +GUIDispatcher.register(defines.events.on_gui_value_changed, CONDITION_VALUE_CHANGED_EVENT, on_condition_value_changed) +GUIDispatcher.register(defines.events.on_gui_text_changed, CONDITION_VALUE_CHANGED_EVENT, on_condition_value_changed) +GUIDispatcher.register(defines.events.on_gui_confirmed, CONDITION_VALUE_CHANGED_EVENT, on_condition_value_confirmed) +GUIDispatcher.register(defines.events.on_gui_selection_state_changed, CONDITION_SURFACE_CHANGED_EVENT, on_condition_surface_changed) +GUIDispatcher.register(defines.events.on_gui_click, CONDITION_SURFACE_RESET_EVENT, on_condition_surface_reset_clicked) + +GUIDispatcher.register(defines.events.on_gui_elem_changed, FURNACE_RECIPE_EVENT, on_furnace_recipe_changed) + +GUIDispatcher.register(defines.events.on_gui_checked_state_changed, RETURN_EXCESS_CHECKED_EVENT, on_return_excess_checked) + +GUIDispatcher.register(defines.events.on_gui_click, SHOW_PRIORITY_GUI_EVENT, on_show_priority_gui) + +return M diff --git a/src/service_furnace.lua b/src/service_furnace.lua index 1d084f6..d0f5533 100644 --- a/src/service_furnace.lua +++ b/src/service_furnace.lua @@ -26,8 +26,22 @@ local M = {} -- local recipe cache, as the mapping can't change during a run -- and we don't need to save or sync this info. -M.recipes_input = {} -- { key=furnace_name, val={ key=ore_name, val=recipe } } -M.recipes_output = {} -- { key=furnace_name, val={ key=item_name, val=recipe } } +global.recipes_input = {} -- { key=furnace_name, val={ key=ore_name, val=recipe } } +global.recipes_output = {} -- { key=furnace_name, val={ key=item_name, val=recipe } } + +local function item_from_recipe(recipe_name) + if recipe_name ~= nil then + local recipe = game.recipe_prototypes[recipe_name] + if recipe ~= nil then + for _, ing in ipairs(recipe.ingredients) do + if ing.type == "item" then + return ing.name + end + end + end + end + return nil +end --[[ Sets info.config.ore_name and info.config.recipe_name. @@ -44,6 +58,8 @@ function M.set_ore_name(info, ore_name) local rp = M.furnace_get_recipe(info.entity, ore_name) if rp ~= nil then recipe_name = rp.name + print(string.format("%s[%s] set ore %s recipe %s", info.entity.name, info.unit_number, + ore_name, recipe_name)) end end cfg.recipe_name = recipe_name @@ -63,6 +79,31 @@ function M.get_recipe_name(info) return (info.config or {}).recipe_name end +function M.set_recipe_name(info, recipe_name) + -- TODO: verify that the recipe is valid + if info.config.recipe_name ~= recipe_name then + info.config.new_recipe_name = recipe_name + GlobalState.queue_reservice(info) + print(string.format("%s[%s] set recipe %s (was %s)", + info.entity.name, info.unit_number, + recipe_name, info.config.recipe_name)) + end +--[[ + if recipe_name then + local recipe = game.recipe_prototypes[recipe_name] + if recipe then + for _, ing in ipairs(recipe.ingredients) do + if ing.type == "item" then + info.config.ore_name = ing.name + print(string.format("%s[%s] set recipe %s item %s", info.entity.name, info.unit_number, + recipe_name, ing.name)) + end + end + end + end +]] +end + --[[ Scan the crafting_categories and find the first matching recipe. @entity used for entity.name and entity.prototype.crafting_categories @@ -109,7 +150,7 @@ Determine the recipe based on the entity and the ore_name. returns the recipe prototype. ]] function M.furnace_get_recipe(entity, ore_name) - return get_recipe(entity, ore_name, M.recipes_input, "has-ingredient-item") + return get_recipe(entity, ore_name, global.recipes_input, "has-ingredient-item") end --[[ @@ -117,7 +158,7 @@ Guess the recipe based on the output inventory content. returns the recipe prototype. ]] function M.furnace_get_recipe_output(entity, item_name) - return get_recipe(entity, item_name, M.recipes_output, "has-product-item") + return get_recipe(entity, item_name, global.recipes_output, "has-product-item") end --[[ @@ -263,6 +304,7 @@ Adjust priority ]] function M.furnace_service(info) local entity = info.entity + local config = info.config local status = entity.status local pri = GlobalState.UPDATE_STATUS.UPDATE_PRI_DEC @@ -274,6 +316,17 @@ function M.furnace_service(info) local o_inv = entity.get_output_inventory() local inv_src = entity.get_inventory(defines.inventory.furnace_source) + -- check for a new_recipe_name + if config.new_recipe_name ~= nil then + if config.recipe_name ~= config.new_recipe_name then + config.recipe_name = config.new_recipe_name + config.ore_name = item_from_recipe(config.recipe_name) + GlobalState.items_inv_to_net(o_inv) + GlobalState.items_inv_to_net(inv_src) + end + config.new_recipe_name = nil + end + -- grab the configured ore and the input ore local old_ore = M.get_ore_name(info) local ore_name = M.furnace_get_ore(info) @@ -286,6 +339,8 @@ function M.furnace_service(info) -- forcibly remove output if the ore changed if ore_name ~= old_ore then + print(string.format("%s[%s] ore changed from %s to %s", entity.name, entity.unit_number, + old_ore, ore_name)) GlobalState.items_inv_to_net(o_inv) elseif not o_inv.is_empty() then @@ -322,6 +377,8 @@ function M.furnace_service(info) if status == defines.entity_status.no_ingredients and not is_short then pri = GlobalState.UPDATE_STATUS.UPDATE_PRI_INC end + else + pri = GlobalState.UPDATE_STATUS.UPDATE_PRI_MAX end return pri end diff --git a/src/service_lab.lua b/src/service_lab.lua index ac3c03a..83c00e5 100644 --- a/src/service_lab.lua +++ b/src/service_lab.lua @@ -9,16 +9,17 @@ local Event = require('__stdlib__/stdlib/event/event') local M = {} -- list of recipes that creates the science pack -M.item_recipes = {} +-- key=item_name, val={ recipe_name, ... } +global.item_recipes = {} -- key=force.name, val={ [item_name]=boolean } -M.force_item_avail = {} +global.force_item_avail = {} --[[ Get the list of recipes that create the specified item. This will not change during a run and does not need to be saved. ]] function M.recipes_for_item(item_name) - local rs = M.item_recipes[item_name] + local rs = global.item_recipes[item_name] if rs == nil then rs = game.get_filtered_recipe_prototypes({ { filter="has-product-item", elem_filters = {{ filter = "name", name = item_name }} } @@ -28,16 +29,16 @@ function M.recipes_for_item(item_name) for k, _ in pairs(rs) do rnt[k] = true end - M.item_recipes[item_name] = rnt + global.item_recipes[item_name] = rnt end return rs end function M.force_item_available(force, item_name) - local spb = M.force_item_avail[force.name] + local spb = global.force_item_avail[force.name] if spb == nil then spb = {} - M.force_item_avail[force.name] = spb + global.force_item_avail[force.name] = spb end -- see if the result is cached local res = spb[item_name] @@ -101,10 +102,10 @@ While we could see which items are afected by looking at the products, it is easier to just reset anything that was determined to NOT be available previously. ]] local function on_research_finished(event) - print_entity_status() + -- print_entity_status() local force = event.research.force if force ~= nil then - local spb = M.force_item_avail[force.name] + local spb = global.force_item_avail[force.name] if spb ~= nil then for item_name, val in pairs(spb) do if val == false then @@ -124,9 +125,9 @@ Not tested. local function on_research_reversed(event) local force = event.research.force if force ~= nil then - local spb = M.force_item_avail[force.name] + local spb = global.force_item_avail[force.name] if spb ~= nil then - M.force_item_avail[force.name] = {} + global.force_item_avail[force.name] = {} end end end diff --git a/src/tables_have_same_keys.lua b/src/tables_have_same_keys.lua index a9360bd..dd43405 100644 --- a/src/tables_have_same_keys.lua +++ b/src/tables_have_same_keys.lua @@ -18,7 +18,7 @@ function M.tables_have_same_keys(tab1, tab2) return true end --- compare two tables that consist of key=count +-- compare two tables that consist of key=value function M.tables_have_same_counts(tab1, tab2) if tab1 == nil or tab2 == nil then return false @@ -36,4 +36,77 @@ function M.tables_have_same_counts(tab1, tab2) return true end +-- compare two flat tables that are iterated using pairs() +function M.same_pairs_flat(tab1, tab2) + if tab1 == nil or tab2 == nil then + return false + end + for k1, v1 in pairs(tab1) do + if tab2[k1] ~= v1 then + return false + end + end + for k2, v2 in pairs(tab2) do + if tab1[k2] ~= v2 then + return false + end + end + return true +end + +function M.same_value(value1, value2) + --print("comparing", serpent.block(value1), "vs", serpent.block(value2)) + -- both nil or different type => false + if type(value2) ~= type(value1) or value1 == nil then + --print(" - not same type or nil") + return false + end + if type(value1) == "table" then + --print(" - comparing tables") + -- same type, both tables + for k1, v1 in pairs(value1) do + local v2 = value2[k1] + if not M.same_value(v1, v2) then + return false + end + end + for k2, v2 in pairs(value2) do + local v1 = value1[k2] + if not M.same_value(v1, v2) then + return false + end + end + return true + end + -- same type, not a table + return (value2 == value1) +end + +-- compare two flat tables that are iterated using pairs() +function M.same_pairs_recursive(tab1, tab2) + if tab1 == nil or tab2 == nil then + return false + end + for k1, v1 in pairs(tab1) do + local v2 = tab2[k1] + if type(v2) ~= type(v1) then + return false + end + if type(v1) == "table" then + if not M.same_pairs_recursive(v1, v2) then + return false + end + end + if v2 ~= v1 then + return false + end + end + for k2, v2 in pairs(tab2) do + if tab1[k2] ~= v2 then + return false + end + end + return true +end + return M