Files
factorio-expcluster/exp_scenario/module/control/mine_depletion.lua
T
bbassieandClaude Opus 5.5 628e4df514 Rework features into a Feature class with per field events
Review points from Cooldude:

- Feature.register(name[, config]) is required again and returns a
  feature object. Each feature has its config, its fields, and its
  overrides, so one registry holds all the data. Only the overrides are
  stored. On load each feature points at its own stored table again.
- The methods are feature:guard(lib), feature:update_config(enabled,
  values), and feature:is_enabled(). Feature.get(name) looks up a
  feature, and commands flag the feature object.
- update_config raises on_config_updated once per changed value, with
  the feature name, path, old value, and new value. The gui refresh only
  reacts to the "enabled" path.
- apply only sets values. on_apply is gone. Lookup sets built from
  lists use Feature.to_set, which is cached by the list itself. A
  replaced list gets a new set, so the sets are also correct after a
  load. Protection exposes getters for its protected names and types.

Web: settings use small inputs, the same size as the config pages.
Fields are shaped like clusterio config field definitions. A field
with an inputComponent renders the component registered with the web
interface, so the role setting now uses core's role input, and other
plugin inputs can be used later.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:03:11 +00:00

285 lines
9.4 KiB
Lua

--[[-- Control - Mine Depletion
Marks mining drills for deconstruction when resources deplete
]]
local Async = require("modules/exp_util/async")
local Feature = require("modules/exp_scenario/features")
local feature = Feature.register("mine_depletion", {
fluid = true, --- @setting fluid When true, checks for for fluid pipes when removing miners
chest = true, --- @setting chest When true, checks for for chest when removing miners
beacon = true, --- @setting beacon When true, checks for for beacon when removing miners
})
local config = feature.config
local floor = math.floor
--- Orders the deconstruction of an entity by its own force
local order_deconstruction_async =
Async.register(function(entity)
--- @cast entity LuaEntity
entity.order_deconstruction(entity.force)
end)
--- Reliability get the drop target of an entity
--- @param entity LuaEntity
--- @return LuaEntity?
local function get_drop_chest(entity)
-- First check the direct drop target
local target = entity.drop_target
if target and (target.type == "container" or target.type == "logistic-container" or target.type == "infinity-container") then
return target
end
-- Then check all entities at the drop position
local entities = entity.surface.find_entities_filtered{
position = entity.drop_position,
type = { "container", "logistic-container", "infinity-container" },
}
return #entities > 0 and entities[1] or nil
end
--- Check if an entity should has checked performed
--- @param entity LuaEntity
--- @return boolean
local function prevent_deconstruction(entity)
-- Already waiting to be deconstructed
if not entity.valid or entity.to_be_deconstructed() then
return true
end
-- Not minable, selectable, or deconstructive
if not entity.minable_flag or not entity.prototype.selectable_in_game or entity.has_flag("not-deconstructable") then
return true
end
-- Is connected to the circuit network
local red_write_connection = entity.get_wire_connector(defines.wire_connector_id.circuit_red, false)
local green_write_connection = entity.get_wire_connector(defines.wire_connector_id.circuit_green, false)
if red_write_connection and red_write_connection.connection_count > 0
or green_write_connection and green_write_connection.connection_count > 0 then
return true
end
return false
end
--- Check if an output chest should be deconstructed
--- @param entity LuaEntity
local function try_deconstruct_output_chest(entity)
-- Get a valid chest as the target
local target = get_drop_chest(entity)
if not target or prevent_deconstruction(target) then
return
end
-- Get all adjacent mining drills and inserters
local target_position = target.position
local entities = target.surface.find_entities_filtered{
type = { "mining-drill", "inserter" },
to_be_deconstructed = false,
area = {
left_top = { x = target_position.x - 1, y = target_position.y - 1 },
right_bottom = { x = target_position.x + 1, y = target_position.y + 1 },
},
}
-- Check if any other entity is using this chest
for _, other in ipairs(entities) do
if other ~= entity and get_drop_chest(other) == target then
return
end
end
-- Deconstruct the chest
order_deconstruction_async:start_after(10, target)
end
--- Check if beacon should be deconstructed
--- @param entity LuaEntity
local function try_deconstruct_nearby_beacon(entity)
-- Get a valid chest as the target
local beacons = entity.get_beacons()
local beacon_in_use = false
-- if beacon is not supported
if not beacons then
return
end
for _, b in pairs(beacons) do
if not b.to_be_deconstructed() then
for _, r in pairs(b.get_beacon_effect_receivers()) do
if r ~= entity then
if not r.to_be_deconstructed() then
beacon_in_use = true
break
end
end
end
-- Deconstruct the beacon
if not beacon_in_use then
order_deconstruction_async:start_after(10, { name = b.name, position = b.position })
end
end
end
end
--- Check if a miner should be deconstructed
--- @param entity LuaEntity
local function try_deconstruct_miner(entity)
-- Check if the miner should be deconstructed
if prevent_deconstruction(entity) then
return
end
-- Check if there are any resources remaining for the miner
local surface = entity.surface
local resources = surface.find_entities_filtered{
type = "resource",
area = entity.mining_area,
}
for _, resource in ipairs(resources) do
if resource.amount > 0 then
return
end
end
-- Deconstruct the miner
order_deconstruction_async:start_after(10, entity)
-- Try deconstruct the output chest
if config.chest then
try_deconstruct_output_chest(entity)
end
-- Try deconstruct the beacon
if config.beacon then
try_deconstruct_nearby_beacon(entity)
end
-- Skip pipe build if not required
if not config.fluid or entity.fluids_count > 1 then
return
end
-- Build pipes if the miner used fluid
local position = entity.position
local create_entity_position = { x = position.x, y = position.y }
local create_entity_param = { name = "entity-ghost", inner_name = "pipe", force = entity.force, position = create_entity_position }
local create_entity = surface.create_entity
create_entity(create_entity_param)
-- Find all the entities to connect to
local bounding_box = entity.bounding_box
local search_area = {
{ bounding_box.left_top.x - 1, bounding_box.left_top.y - 1 },
{ bounding_box.right_bottom.x + 1, bounding_box.right_bottom.y + 1 },
}
local entities = surface.find_entities_filtered{ area = search_area, type = { "mining-drill", "pipe", "pipe-to-ground", "infinity-pipe" } }
local ghosts = surface.find_entities_filtered{ area = search_area, ghost_type = { "pipe", "pipe-to-ground", "infinity-pipe" } }
table.insert_array(entities, ghosts)
-- Check which directions to add pipes in
local pos_x, pos_y, neg_x, neg_y = false, false, false, false
for _, other in ipairs(entities) do
if (other.position.x > position.x) and (other.position.y == position.y) then
pos_x = true
elseif (other.position.x < position.x) and (other.position.y == position.y) then
neg_x = true
elseif (other.position.x == position.x) and (other.position.y > position.y) then
pos_y = true
elseif (other.position.x == position.x) and (other.position.y < position.y) then
neg_y = true
end
end
-- Build the pipes
if pos_x then
create_entity_position.y = floor(position.y)
for x = position.x + 1, bounding_box.right_bottom.x do
create_entity_position.x = x
create_entity(create_entity_param)
end
end
if neg_x then
create_entity_position.y = floor(position.y)
for x = floor(bounding_box.left_top.x), floor(position.x - 1) do
create_entity_position.x = x
create_entity(create_entity_param)
end
end
if pos_y then
create_entity_position.x = floor(position.x)
for y = floor(position.y + 1), floor(bounding_box.right_bottom.y) do
create_entity_position.y = y
create_entity(create_entity_param)
end
end
if neg_y then
create_entity_position.x = floor(position.x)
for y = floor(bounding_box.left_top.y), floor(position.y - 1) do
create_entity_position.y = y
create_entity(create_entity_param)
end
end
end
local max_quality = ""
for _, q in pairs(prototypes.quality) do
if not q.next then
max_quality = q.name
end
end
--- Get the max mining radius
local max_mining_radius = 0
for _, proto in pairs(prototypes.get_entity_filtered{ { filter = "type", type = "mining-drill" } }) do
if proto.mining_drill_radius then
max_mining_radius = math.max(assert(proto.get_mining_drill_radius(max_quality)), max_mining_radius)
end
end
--- Try deconstruct a miner when its resources deplete
--- @param event EventData.on_resource_depleted
local function on_resource_depleted(event)
local resource = event.entity
if resource.prototype.infinite_resource then
return
end
-- Find all mining drills within the area
local position = resource.position
local drills = resource.surface.find_entities_filtered{
type = "mining-drill",
area = {
left_top = { x = position.x - max_mining_radius, y = position.y - max_mining_radius },
right_bottom = { x = position.x + max_mining_radius, y = position.y + max_mining_radius },
},
}
-- Check which could have reached this resource
for _, drill in pairs(drills) do
local radius = drill.prototype.mining_drill_radius
local drill_position = drill.position
local dx = math.abs(drill_position.x - position.x)
local dy = math.abs(drill_position.y - position.y)
if dx <= radius and dy <= radius then
try_deconstruct_miner(drill)
end
end
end
local e = defines.events
return feature:guard{
events = {
[e.on_resource_depleted] = on_resource_depleted,
},
}