diff --git a/data.lua b/data.lua index 82b2ae063..c8eadc049 100644 --- a/data.lua +++ b/data.lua @@ -7,6 +7,7 @@ require "prototypes/recipe-categories" require "prototypes/fuel-categories" require "prototypes/module-categories" require "prototypes/circuit-connector-definitions" +require "prototypes/farming-utils" require "prototypes/keyboard-shortcuts" -- Increase empty barrel stack size in order to prevent inserter deadlocks. https://github.com/pyanodon/pybugreports/issues/314 diff --git a/migrations/farm-optimizations.lua b/migrations/farm-optimizations.lua new file mode 100644 index 000000000..a9e7ab390 --- /dev/null +++ b/migrations/farm-optimizations.lua @@ -0,0 +1,90 @@ +storage.farm_buildings = storage.farm_buildings or {} +storage.farming_deathrattles = storage.farming_deathrattles or {} + +local function base_name(name) + -- TODO: Find a method that avoids two searches? + local is_turd = not not name:find("%-turd") + -- keep suffix if necessary while allowing other building suffixes + return name:gsub("%-mk..+", is_turd and "-turd" or "") +end + +-- animal, plant, or fungi? +local function get_kingdom(entity) + local farm_data = storage.farm_prototypes[base_name(entity.name)] + if farm_data then return farm_data.domain end +end + +local function get_default_module(entity) + local farm_data = storage.farm_prototypes[base_name(entity.name)] + if farm_data then return farm_data.default_module end +end + +local function register_sacrifice(manager, farm) + manager.get_inventory(defines.inventory.crafter_input).insert { + name = "pyfarm-internal-item", + count = 1, + health = 0.5, + } + storage.farming_deathrattles[script.register_on_object_destroyed(manager.get_inventory(defines.inventory.crafter_input)[1].item)] = farm.unit_number +end + +for _, metadata in pairs {storage.enabled_farm_buildings, storage.disabled_farm_buildings} do + for _, entity in pairs(metadata) do + if entity.valid and storage.farm_prototypes[base_name(entity.name)] then + local default_module = get_default_module(entity) + + local manager = entity.surface.create_entity { + name = "pyfarm-internal-manager", + position = entity.position, + force = entity.force + } + local monitor = entity.surface.create_entity { + name = "pyfarm-internal-monitor", + position = entity.position, + force = entity.force + } + local warning + + -- connect source, manager, mimic, and (?) monitor + manager.get_wire_connector(defines.wire_connector_id.circuit_green, true).connect_to(monitor.get_wire_connector(defines.wire_connector_id.circuit_green, true), false, defines.wire_origin.script) + + local active = not entity.get_module_inventory().is_empty() + entity.active = active + entity.custom_status = not active and { + diode = defines.entity_status_diode.red, + label = default_module and {"entity-status.requires-module", default_module, prototypes.item[default_module].localised_name} or {"entity-status.requires-module-reproductive-complex"} + } or nil + + -- set circuit settings + local manager_behaviour = manager.get_or_create_control_behavior() + manager_behaviour.circuit_enable_disable = true + manager_behaviour.circuit_condition = { + comparator = active and "=" or "≠", + constant = 0, + first_signal = {name = active and "signal-everything" or "signal-anything", type = "virtual"} + } + + monitor.proxy_target_entity = entity + monitor.proxy_target_inventory = defines.inventory.crafter_modules + + -- update warning icon and crafting progress + if not active then + warning = py.draw_error_sprite(entity, "no_module_" .. get_kingdom(entity), 0, 30) + if entity.is_crafting() then + entity.crafting_progress = 0.0001 + entity.bonus_progress = 0 + end + end + + -- save data and register event + script.register_on_object_destroyed(entity) + storage.farm_buildings[entity.unit_number] = {farm = entity, manager = manager, monitor = monitor, warning = warning} + register_sacrifice(manager, entity) + end + end +end + +-- remove excess data +storage.enabled_farm_buildings = nil +storage.disabled_farm_buildings = nil +storage.next_farm_index = nil diff --git a/prototypes/farming-utils.lua b/prototypes/farming-utils.lua new file mode 100644 index 000000000..6981708ad --- /dev/null +++ b/prototypes/farming-utils.lua @@ -0,0 +1,74 @@ +data:extend { -- entities and things to manage farming buildings + { + type = "recipe-category", + name = "pyfarm-filler-category", + hidden = true, + hidden_in_factoriopedia = true + }, + { -- hidden recipe used to check if machine is working + type = "recipe", + name = "pyfarm-internal-recipe", + icon = util.empty_icon().icon, + category = "pyfarm-filler-category", + ingredients = {{type = "item", name = "pyfarm-internal-item", amount = 1, ignored_by_stats = 1}}, + hidden = true, + hidden_in_factoriopedia = true + }, + { -- hidden item for recipe and signals, can use existing item but this one is garunteed to work + type = "item", + name = "pyfarm-internal-item", + icon = util.empty_icon().icon, + stack_size = 1, + hidden = true, + hidden_in_factoriopedia = true + }, + { -- hidden assembling machine to craft the aforementioned recipe + type = "assembling-machine", + name = "pyfarm-internal-manager", + icon = util.empty_icon().icon, + collision_mask = {layers = {}}, + flags = { + "placeable-off-grid", + "not-repairable", + "not-on-map", + "not-blueprintable", + "not-deconstructable", + "no-copy-paste", + "not-upgradable", + "placeable-neutral", + "no-automated-item-removal", + "no-automated-item-insertion" + }, + allow_copy_paste = false, + selectable_in_game = false, + energy_usage = "1W", + energy_source = {type = "void"}, + crafting_categories = {"pyfarm-filler-category"}, + fixed_recipe = "pyfarm-internal-recipe", + crafting_speed = 60, + hidden = true, + hidden_in_factoriopedia = true + }, + { -- hidden proxy container to monitor module inventory + type = "proxy-container", + name = "pyfarm-internal-monitor", + icon = util.empty_icon().icon, + draw_inventory_content = false, + collision_mask = {layers = {}}, + flags = { + "not-rotatable", + "placeable-neutral", + "placeable-off-grid", + "not-repairable", + "not-on-map", + "not-deconstructable", + "not-blueprintable", + "hide-alt-info", + "not-upgradable" + }, + allow_copy_paste = false, + selectable_in_game = false, + hidden = true, + hidden_in_factoriopedia = true + } +} diff --git a/scripts/farming/farming.lua b/scripts/farming/farming.lua index 1dd00e619..9f3959307 100644 --- a/scripts/farming/farming.lua +++ b/scripts/farming/farming.lua @@ -1,17 +1,21 @@ Farming = {} -local function validate_farm_building_list(farm_buildings, throw) +local function base_name(name) + -- TODO: Find a method that avoids two searches? + local is_turd = not not name:find("%-turd") + -- keep suffix if necessary while allowing other building suffixes + return name:gsub("%-mk..+", is_turd and "-turd" or "") +end + +local function validate_farm_building_list(farm_buildings_list, throw) local modules = prototypes.get_item_filtered {{filter = "type", type = "module"}} ---@as table> two level table containing buildings indexed by their base (mk-less) name local buildings = {} local crafting_machines = prototypes.get_entity_filtered {{filter = "crafting-machine"}} -- This early search and sort lets us avoid o^n searching below for building_name in pairs(crafting_machines) do - -- TODO: Find a method that avoids two searches? - local is_turd = not not building_name:find("%-turd") - -- keep suffix if necessary while allowing other building suffixes - local basename = building_name:gsub("%-mk..+", is_turd and "-turd" or "") - if farm_buildings[basename] then + local basename = base_name(building_name) + if farm_buildings_list[basename] then buildings[basename] = buildings[basename] or {} buildings[basename][building_name] = true end @@ -19,21 +23,21 @@ local function validate_farm_building_list(farm_buildings, throw) -- Assigns nil or errors depending on `throw` local result = throw and error or log - for entity_name, farm_prototype in pairs(farm_buildings) do + for entity_name, farm_prototype in pairs(farm_buildings_list) do -- No buildings with this base name if not buildings[entity_name] then - farm_buildings[entity_name] = result(("Farm building \"%s\" has no associated crafting machines"):format(entity_name)) + farm_buildings_list[entity_name] = result(("Farm building \"%s\" has no associated crafting machines"):format(entity_name)) goto next_farm_prototype end -- No modules with this name if farm_prototype.default_module ~= nil and not modules[farm_prototype.default_module] then - farm_buildings[entity_name] = result(("Invalid default module \"%s\" for farm building \"%s\""):format(farm_prototype.default_module, entity_name)) + farm_buildings_list[entity_name] = result(("Invalid default module \"%s\" for farm building \"%s\""):format(farm_prototype.default_module, entity_name)) goto next_farm_prototype end -- Unspecified or invalid domain local domain = farm_prototype.domain if not domain or not (domain == "animal" or domain == "plant" or domain == "fungi") then - farm_buildings[entity_name] = result(("Invalid domain \"%s\" for farm building \"%s\". Expected 'animal', 'plant', or 'fungi'"):format(domain or "nil", entity_name)) + farm_buildings_list[entity_name] = result(("Invalid domain \"%s\" for farm building \"%s\". Expected 'animal', 'plant', or 'fungi'"):format(domain or "nil", entity_name)) goto next_farm_prototype end -- Wow, so valid @@ -44,7 +48,7 @@ end ---@as table> ---Contains key-value pairs of `{farm_name = {default_module = farm_module, domain = farm_domain, requires = farm_mod}` -- See `scripts/farming/farm-build-list.lua` for an example -local farm_buildings = require "farm-building-list" +local farm_buildings_list = require "farm-building-list" ---register_type registers a farm for module restrictions ---@param farm_name string name of farm building without -mkxx suffix @@ -52,7 +56,7 @@ local farm_buildings = require "farm-building-list" ---@param default_module string function Farming.register_type(farm_name, domain, default_module) log("remote registered farm \'" .. farm_name .. "\' (" .. domain .. ")") - storage.farm_prototypes = storage.farm_prototypes or farm_buildings + storage.farm_prototypes = storage.farm_prototypes or farm_buildings_list storage.farm_prototypes[farm_name] = {default_module = default_module, domain = domain} validate_farm_building_list(storage.farm_prototypes, true) end @@ -72,101 +76,137 @@ remote.add_interface("pyfarm", { -- animal, plant, or fungi? function Farming.get_kingdom(entity) - local is_turd = not not entity.name:find("%-turd") - local name = entity.name:gsub("%-mk..+", is_turd and "-turd" or "") - local farm_data = storage.farm_prototypes[name] + local farm_data = storage.farm_prototypes[base_name(entity.name)] if farm_data then return farm_data.domain end end function Farming.get_default_module(entity) - local is_turd = not not entity.name:find("%-turd") - local name = entity.name:gsub("%-mk..+", is_turd and "-turd" or "") - local farm_data = storage.farm_prototypes[name] + local farm_data = storage.farm_prototypes[base_name(entity.name)] if farm_data then return farm_data.default_module end end -function Farming.disable_machine(entity) - local kingdom = Farming.get_kingdom(entity) - if not kingdom then return end - local default_module = Farming.get_default_module(entity) - entity.active = false - if default_module then - entity.custom_status = { - diode = defines.entity_status_diode.red, - label = {"entity-status.requires-module", default_module, prototypes.item[default_module].localised_name} - } - else - entity.custom_status = { - diode = defines.entity_status_diode.red, - label = {"entity-status.requires-module-reproductive-complex"} - } - end - storage.disabled_farm_buildings[entity.unit_number] = entity - script.register_on_object_destroyed(entity) - if entity.is_crafting() then - entity.crafting_progress = 0.0001 - entity.bonus_progress = 0 - end - py.draw_error_sprite(entity, "no_module_" .. kingdom, 61, 30) -end - -function Farming.enable_machine(entity) - storage.disabled_farm_buildings[entity.unit_number] = nil - entity.active = true - entity.custom_status = nil - storage.enabled_farm_buildings[#storage.enabled_farm_buildings + 1] = entity -end - py.on_event(py.events.on_init(), function() - storage.disabled_farm_buildings = storage.disabled_farm_buildings or {} - storage.enabled_farm_buildings = storage.enabled_farm_buildings or {} - storage.farm_prototypes = farm_buildings + storage.farm_buildings = storage.farm_buildings or {} + storage.farming_deathrattles = storage.farming_deathrattles or {} + storage.farm_prototypes = farm_buildings_list validate_farm_building_list(storage.farm_prototypes) - storage.next_farm_index = storage.next_farm_index or 1 end) +---function to register an event firing when the manager is enabled via the circuit network, i.e. detects a change in the farm module inventory +---this works by creating an item with data (health in this case) and registering it to fire an event when destroyed (consumed for crafting) +---we save the associated farm data by unit number in storage via a key associated with that specific item, returned from script.register_on_object_destroyed +local function register_sacrifice(manager, farm) + manager.get_inventory(defines.inventory.crafter_input).insert{ + name = "pyfarm-internal-item", + count = 1, + health = 0.5, + } + storage.farming_deathrattles[script.register_on_object_destroyed(manager.get_inventory(defines.inventory.crafter_input)[1].item)] = farm.unit_number +end + py.on_event(py.events.on_built(), function(event) local entity = event.entity - if entity.type == "assembling-machine" then Farming.disable_machine(entity) end -end) + if not storage.farm_prototypes[base_name(entity.name)] then return end -py.on_event(defines.events.on_object_destroyed, function(event) - local unit_number = event.useful_id - if not unit_number then return end - storage.disabled_farm_buildings[unit_number] = nil + local default_module = Farming.get_default_module(entity) + -- create entities to track modules + local manager = entity.surface.create_entity{ + name = "pyfarm-internal-manager", + position = entity.position, + force = entity.force + } + local monitor = entity.surface.create_entity{ + name = "pyfarm-internal-monitor", + position = entity.position, + force = entity.force + } + + -- connect manager and monitor such that any changes to the module inventory are sent via circuit network + manager.get_wire_connector(defines.wire_connector_id.circuit_green, true).connect_to(monitor.get_wire_connector(defines.wire_connector_id.circuit_green, true), false, defines.wire_origin.script) + + -- set circuit settings, such that when any item is detected in the module inventory the crafter will turn on + local manager_behaviour = manager.get_or_create_control_behavior() + manager_behaviour.circuit_enable_disable = true + manager_behaviour.circuit_condition = { + comparator = "≠", + constant = 0, + first_signal = { name = "signal-anything", type = "virtual" } + } + -- set the monitor to point to the module inventory + monitor.proxy_target_entity = entity + monitor.proxy_target_inventory = defines.inventory.crafter_modules + -- turn off the farm, it will turn on when modules are inserted + entity.active = false + entity.custom_status = { + diode = defines.entity_status_diode.red, + label = default_module and {"entity-status.requires-module", default_module, prototypes.item[default_module].localised_name} or {"entity-status.requires-module-reproductive-complex"} + } + + -- save farm data in storage and register this farm to fire an event when modules are inserted + -- also register the farm to fire an event when it is destroyed so we can clean up + script.register_on_object_destroyed(entity) + storage.farm_buildings[entity.unit_number] = {farm = entity, manager = manager, monitor = monitor, warning = py.draw_error_sprite(entity, "no_module_" .. Farming.get_kingdom(entity), 0, 30)} + register_sacrifice(manager, entity) end) --- render warning icons -py.register_on_nth_tick(59, "Farming59", "pyal", function(event) - for unit_number, farm in pairs(storage.disabled_farm_buildings) do - if not farm.valid then - storage.disabled_farm_buildings[unit_number] = nil - elseif farm.get_module_inventory().is_empty() then - py.draw_error_sprite(farm, "no_module_" .. Farming.get_kingdom(farm), 61, 30) - else - Farming.enable_machine(farm) +-- event fired when a thing registered via script.register_on_destroyed is destroyed +py.on_event(defines.events.on_object_destroyed, function(event) + -- skip other event categories we dont care about + if not event.useful_id or not event.registration_number then return end + -- other mods can use it too, make sure its our event (registration number is unique across all mods and all registrations) + if storage.farming_deathrattles[event.registration_number] then + + -- handle deathrattles from module inventories changing + local metadata = storage.farm_buildings[storage.farming_deathrattles[event.registration_number]] + -- if anything is invalid destroy it all + if not metadata then return elseif not (metadata.farm and metadata.farm.valid and metadata.manager and metadata.manager.valid and metadata.monitor and metadata.monitor.valid) then + if metadata.farm and metadata.farm.valid then metadata.farm.destroy() end + if metadata.manager and metadata.manager.valid then metadata.manager.destroy() end + if metadata.monitor and metadata.monitor.valid then metadata.monitor.destroy() end + storage.farm_buildings[storage.farming_deathrattles[event.registration_number]] = nil + storage.farming_deathrattles[event.registration_number] = nil + return end - end -end) + storage.farming_deathrattles[event.registration_number] = nil -- remove unused reference + + local farm = metadata.farm + local manager = metadata.manager + local default_module = Farming.get_default_module(farm) + -- reset the event trigger + register_sacrifice(manager, farm) + -- it should be ON if the module inventory is not empty + local active = not farm.get_module_inventory().is_empty() + -- skip if the state hasnt changed (required to skip triggering twice) + if active ~= farm.disabled_by_script then return end + + farm.active = active + farm.custom_status = not active and { + diode = defines.entity_status_diode.red, + label = default_module and {"entity-status.requires-module", default_module, prototypes.item[default_module].localised_name} or {"entity-status.requires-module-reproductive-complex"} + } or nil + -- set manager to enable if everything = 0 (no modules exist) or anything ~= 0 (modules exist) + manager.get_or_create_control_behavior().circuit_condition = { + comparator = active and "=" or "≠", + constant = 0, + first_signal = { name = active and "signal-everything" or "signal-anything", type = "virtual" } + } --- every 2 seconds, check 1/8th of farm buildings for empty module inventory -py.register_on_nth_tick(121, "Farming121", "pyal", function() - local farm_count = #storage.enabled_farm_buildings - if farm_count == 0 then return end - local first_index_checked_this_tick = storage.next_farm_index - for i = 1, math.ceil(farm_count / 8) do - local farm = storage.enabled_farm_buildings[storage.next_farm_index] - - if not farm or not farm.valid then - table.remove(storage.enabled_farm_buildings, storage.next_farm_index) - elseif farm.get_module_inventory().is_empty() then - Farming.disable_machine(farm) - table.remove(storage.enabled_farm_buildings, storage.next_farm_index) - else - storage.next_farm_index = storage.next_farm_index + 1 + -- update warning icon and crafting progress + if not active then + metadata.warning = py.draw_error_sprite(farm, "no_module_" .. Farming.get_kingdom(farm), 0, 30) + if farm.is_crafting() then + farm.crafting_progress = 0.0001 + farm.bonus_progress = 0 + end + else -- building is working, remove warning + metadata.warning.destroy() end - if storage.next_farm_index > #storage.enabled_farm_buildings then storage.next_farm_index = 1 end - if storage.next_farm_index == first_index_checked_this_tick then return end + elseif storage.farm_buildings[event.useful_id] then + local metadata = storage.farm_buildings[event.useful_id] + -- destroy support entities and remove storage reference + metadata.manager.destroy() + metadata.monitor.destroy() + storage.farm_buildings[event.useful_id] = nil end -end) +end) \ No newline at end of file