544 lines
18 KiB
Lua

local util = require 'script.util'
local gui = require 'script.gui'
local settings_parser = require 'script.settings-parser'
local recipe_selector = require 'script.recipe-selector'
local config = require 'config'
local signals = require 'script.signals'
local _M = {}
local combinator_mt = {__index = _M}
local CHEST_POSITION_NAMES = {'behind', 'left', 'right', 'behind-left', 'behind-right'}
local CHEST_POSITIONS = {}; for key, name in pairs(CHEST_POSITION_NAMES) do CHEST_POSITIONS[name] = key; end
local CHEST_DIRECTIONS = {
[CHEST_POSITIONS.behind] = 180,
[CHEST_POSITIONS.right] = 90,
[CHEST_POSITIONS.left] = -90,
[CHEST_POSITIONS['behind-right']] = 135,
[CHEST_POSITIONS['behind-left']] = -135,
}
local STATUS_SIGNALS = {}
for name, signal in pairs(config.MACHINE_STATUS_SIGNALS) do
if defines.entity_status[name] then
STATUS_SIGNALS[defines.entity_status[name]] = signal
end
end
_M.settings_parser = settings_parser {
chest_position = {'c', 'int'},
mode = {'m', 'string'},
discard_items = {'d', 'bool'},
discard_fluids = {'f', 'bool'},
empty_inserters = {'i', 'bool'},
craft_until_zero = {'z', 'bool'},
read_recipe = {'r', 'bool'},
read_speed = {'s', 'bool'},
read_machine_status = {'st', 'bool'},
wait_for_output_to_clear = {'wo', 'bool'},
}
-- General housekeeping
function _M.init_global()
global.cc = global.cc or {}
global.cc.data = global.cc.data or {}
global.cc.ordered = global.cc.ordered or {}
global.cc.inserter_empty_queue = {}
end
function _M.on_load()
for _, combinator in pairs(global.cc.data) do setmetatable(combinator, combinator_mt); end
end
-- Lifecycle events
function _M.create(entity)
local combinator = setmetatable({
entity = entity,
control_behavior = entity.get_or_create_control_behavior(),
module_chest = entity.surface.create_entity {
name = config.MODULE_CHEST_NAME,
position = entity.position,
force = entity.force,
create_build_effect_smoke = false,
},
settings = _M.settings_parser:read_or_default(entity, util.deepcopy(config.CC_DEFAULT_SETTINGS)),
inventories = {},
items_to_ignore = {},
last_flying_text_tick = -config.FLYING_TEXT_INTERVAL,
enabled = true,
last_recipe = false,
}, combinator_mt)
combinator.module_chest.destructible = false
combinator.inventories.module_chest = combinator.module_chest.get_inventory(defines.inventory.chest)
global.cc.data[entity.unit_number] = combinator
table.insert(global.cc.ordered, combinator)
combinator:find_assembler()
combinator:find_chest()
-- Other combinators can use the module chest as overflow output, so let them know it's there
_M.update_chests(entity.surface, combinator.module_chest)
end
function _M.mark_for_deconstruction(entity)
local combinator = global.cc.data[entity.surface.find_entity(config.CC_NAME, entity.position).unit_number]
combinator.enabled = false
combinator:update()
end
function _M.cancel_deconstruction(entity)
local combinator = global.cc.data[entity.surface.find_entity(config.CC_NAME, entity.position).unit_number]
combinator.enabled = true
combinator:update()
end
function _M.fix_undo_deconstruction(entity, player_index)
local combinator = global.cc.data[entity.unit_number]
local player = player_index and game.get_player(player_index)
local force = player and player.force or entity.force
entity.cancel_deconstruction(force, player)
combinator.module_chest.order_deconstruction(force, player)
end
function _M.destroy_by_robot(entity)
local combinator_entity = entity.surface.find_entity(config.CC_NAME, entity.position)
if not combinator_entity then return; end
_M.destroy(combinator_entity)
combinator_entity.destroy()
end
function _M.destroy(entity, player_index)
local unit_number = entity.unit_number
local combinator = global.cc.data[unit_number]
if player_index then
local inventory = combinator.inventories.module_chest
if not inventory.is_empty() then
local target = player_index and game.get_player(player_index).get_inventory(defines.inventory.character_main)
for i = 1, #inventory do
local stack = inventory[i]
if stack.valid_for_read then
local r = target and target.insert(stack) or 0
if r < stack.count then
stack.count = stack.count - r
-- Clone the entity as replacement and tell the player the inventory is full
game.get_player(player_index).print{'inventory-restriction.player-inventory-full', stack.prototype.localised_name}
-- Replace the entity if a player was trying to pick it up
local old_entity = combinator.entity
local old_cb = combinator.control_behavior
combinator.entity = old_entity.clone{position = old_entity.position}
combinator.control_behavior = combinator.entity.get_or_create_control_behavior()
global.cc.data[unit_number] = nil
global.cc.data[combinator.entity.unit_number] = combinator
for _, connection in pairs(old_entity.circuit_connection_definitions) do
combinator.entity.connect_neighbour(connection)
end
old_entity.destroy()
return true -- Inidcate that the original entity was destroyed
else stack.clear(); end
end
end
end
end
-- Notify other combinators that the chest was destroyed
_M.update_chests(entity.surface, combinator.module_chest, true)
if player_index then combinator.module_chest.destroy(); end
settings_parser.destroy(entity)
signals.cache.drop(entity)
global.cc.data[unit_number] = nil
for k, v in pairs(global.cc.ordered) do
if v.entity.unit_number == unit_number then
table.remove(global.cc.ordered, k)
break
end
end
end
function _M.update_assemblers(surface, assembler, ignore)
local combinators = surface.find_entities_filtered {
area = util.area(assembler.prototype.selection_box):expand(config.ASSEMBLER_SEARCH_DISTANCE) + assembler.position,
name = config.CC_NAME,
}
for _, entity in pairs(combinators) do global.cc.data[entity.unit_number]:find_assembler(ignore and assembler or nil); end
end
function _M.update_chests(surface, chest, ignore)
local combinators = surface.find_entities_filtered {
area = util.area(chest.prototype.selection_box):expand(config.CHEST_SEARCH_DISTANCE) + chest.position,
name = config.CC_NAME,
}
for _, entity in pairs(combinators) do global.cc.data[entity.unit_number]:find_chest(ignore and chest or nil); end
end
function _M:update()
local params = {}
if self.enabled and self.assembler and self.assembler.valid then
self.assembler.active = true
if self.settings.mode == 'w' then
self:set_recipe()
end
if self.settings.mode == 'r' then
if self.settings.read_recipe then self:read_recipe(params); end
if self.settings.read_speed then self:read_speed(params); end
if self.settings.read_machine_status then self:read_machine_status(params); end
end
end
self.control_behavior.parameters = params
end
function _M:open(player_index)
local root = gui.entity(self.entity, {
title_elements = {
gui.button('open-module-chest'),
gui.dropdown('chest-position', CHEST_POSITION_NAMES, self.settings.chest_position, {tooltip=true}),
},
gui.section {
name = 'mode',
gui.radio('w', self.settings.mode, {locale='mode-write', tooltip=true}),
gui.radio('r', self.settings.mode, {locale='mode-read', tooltip=true}),
},
gui.section {
name = 'misc',
gui.checkbox('wait-for-output-to-clear', self.settings.wait_for_output_to_clear, {tooltip = true}),
gui.checkbox('discard-items', self.settings.discard_items),
gui.checkbox('discard-fluids', self.settings.discard_fluids),
gui.checkbox('empty-inserters', self.settings.empty_inserters),
gui.checkbox('craft-until-zero', self.settings.craft_until_zero, {tooltip = true}),
gui.checkbox('read-recipe', self.settings.read_recipe),
gui.checkbox('read-speed', self.settings.read_speed),
gui.checkbox('read-machine-status', self.settings.read_machine_status),
}
}):open(player_index)
self:update_disabled_checkboxes(root)
end
function _M:on_checked_changed(name, state, element)
local category, name = name:gsub(':.*$', ''), name:gsub('^.-:', ''):gsub('-', '_')
if category == 'mode' then
self.settings.mode = name
for _, el in pairs(element.parent.children) do
if el.type == 'radiobutton' then
local _, _, el_name = gui.parse_entity_gui_name(el.name)
el.state = el_name == 'mode:'..name
end
end
end
if category == 'misc' then self.settings[name] = state; end
if name == 'craft_until_zero' and self.settings.craft_until_zero then
self.last_recipe = nil
end
self:update_disabled_checkboxes(gui.get_root(element))
self.settings_parser:update(self.entity, self.settings)
end
function _M:update_disabled_checkboxes(root)
self:disable_checkbox(root, 'misc:discard-items', 'w')
self:disable_checkbox(root, 'misc:discard-fluids', 'w')
self:disable_checkbox(root, 'misc:empty-inserters', 'w')
self:disable_checkbox(root, 'misc:craft-until-zero', 'w')
self:disable_checkbox(root, 'misc:wait-for-output-to-clear', 'w')
self:disable_checkbox(root, 'misc:read-recipe', 'r')
self:disable_checkbox(root, 'misc:read-speed', 'r')
self:disable_checkbox(root, 'misc:read-machine-status', 'r')
end
function _M:disable_checkbox(root, name, mode)
local checkbox = gui.find_element(root, gui.name(self.entity, name))
checkbox.enabled = self.settings.mode == mode
end
function _M:on_selection_changed(name, selected)
if name == 'title:chest-position:value' then
self.settings.chest_position = selected
self.settings_parser:update(self.entity, self.settings)
self:find_chest()
end
end
function _M:on_click(name, element)
if name == 'title:open-module-chest' then
game.get_player(element.player_index).opened = self.module_chest
end
end
-- Other stuff
function _M:read_recipe(params)
local recipe = self.assembler.get_recipe()
if recipe then
table.insert(params, {
signal = recipe_selector.get_signal(recipe.name),
count = 1,
index = 1,
})
end
end
function _M:read_speed(params)
local count = self.assembler.crafting_speed * 100
table.insert(params, {
signal = {type = 'virtual', name = config.SPEED_SIGNAL_NAME},
count = count,
index = 2,
})
end
function _M:read_machine_status(params)
local signal = STATUS_SIGNALS[self.assembler.status or "A dummy string to avoid indexing by nil"]
if signal == nil then return end
table.insert(params, {
signal = {type = 'virtual', name = signal},
count = 1,
index = 3,
})
end
function _M:set_recipe()
local changed, recipe
if self.settings.craft_until_zero then
if not self.last_recipe or not signals.signal_present(self.entity) then
local highest = signals.watch_highest_presence(self.entity)
if highest then recipe = self.entity.force.recipes[highest.signal.name]
else recipe = nil; end
self.last_recipe = recipe
else recipe = self.last_recipe; end
else
changed, recipe = recipe_selector.get_recipe(self.entity, nil, self.last_recipe and self.last_recipe.name)
if changed then self.last_recipe = recipe
else recipe = self.last_recipe; end
end
if recipe and (recipe.hidden or not recipe.enabled) then recipe = nil; end
local a_recipe = self.assembler.get_recipe()
-- Move items if necessary
if a_recipe and ((not recipe) or recipe ~= a_recipe) then
local success, error = self:move_items()
if not success then return self:on_chest_full(error); end
if self.settings.empty_inserters then
success, error = self:empty_inserters()
if not success then return self:on_chest_full(error); end
local tick = game.tick + config.INSERTER_EMPTY_DELAY
global.cc.inserter_empty_queue[tick] = global.cc.inserter_empty_queue[tick] or {}
table.insert(global.cc.inserter_empty_queue[tick], self)
end
-- Clear fluidboxes
if self.settings.discard_fluids then
for i=1, #self.assembler.fluidbox do self.assembler.fluidbox[i] = nil; end
end
end
if recipe ~= a_recipe then
-- Move modules if necessary
if recipe then self:move_modules(recipe); end
-- Finally attempt to switch the recipe
self.assembler.set_recipe(recipe)
local new_recipe = self.assembler.get_recipe()
if new_recipe and new_recipe ~= recipe then self.assembler.set_recipe(nil); end --TODO: Some notification?
end
-- Move modules and items back into the machine
self:insert_modules()
self:insert_items()
return true
end
function _M:move_modules(recipe)
local target = self.inventories.module_chest
local inventory = self.inventories.assembler.modules
for i = 1, #inventory do
local stack = inventory[i]
if stack.valid_for_read then
local limitations = util.module_limitations()[stack.name]
--TODO: Deal with not enough space in the chest
if limitations and not limitations[recipe.name] then target.insert(stack); end
end
end
end
function _M:insert_modules()
local inventory = self.inventories.module_chest
if inventory.is_empty() then return; end
local target = self.inventories.assembler.modules
for i = 1, #inventory do
local stack = inventory[i]
if stack.valid_for_read then
local r = target.insert(stack)
if r < stack.count then stack.count = stack.count - r
else stack.clear(); end
end
end
end
function _M:insert_items()
local inventory = self.inventories.chest
if not inventory or not inventory.valid or inventory.is_empty() then return; end
local target = self.inventories.assembler.input
for i = 1, #inventory do
local stack = inventory[i]
if stack.valid_for_read then
local r = target.insert(stack)
if r < stack.count then stack.count = stack.count - r
else stack.clear(); end
end
end
end
function _M:move_items()
if self.settings.wait_for_output_to_clear and not self.inventories.assembler.output.is_empty() then
return false, 'waiting-for-output'
end
if self.settings.discard_items then return true; end
local target = self:get_chest_inventory()
-- Compensate for half-finished crafts
-- Do this first to avoid losing a lot of items
if self.assembler.crafting_progress > 0 then
local success = true
for _, ing in pairs(self.assembler.get_recipe().ingredients) do
if ing.type == 'item' then
if not target then return false, 'no-chest'; end
local r = target.insert{name = ing.name, count = ing.amount}
if r < ing.amount then success = false; end
end
end
self.assembler.crafting_progress = 0
if not success then return false, 'chest-full'; end
end
-- Clear the assembler inventories
-- This may become somewhat problematic if the input items can be moved, but the output can't, since inserters will
-- continue to replace the items that were removed. I guess that's up to the player to deal with tho...
for _, inventory in pairs{self.inventories.assembler.input, self.inventories.assembler.output} do
for i=1, #inventory do
local stack = inventory[i]
if stack.valid_for_read then
if not target then return false, 'no-chest'; end
local r = target.insert(stack)
if r < stack.count then
stack.count = stack.count - r -- Make sure the items don't get duplicated
return false, 'chest-full'
end
inventory[i].clear()
end
end
end
return true
end
function _M:on_chest_full(error)
-- Prevent the assembler from crafting any more shit
self.assembler.active = false
if game.tick - self.last_flying_text_tick >= config.FLYING_TEXT_INTERVAL then
self.last_flying_text_tick = game.tick
self.entity.surface.create_entity {
name = 'flying-text',
position = self.entity.position,
text = {'crafting_combinator_gui.switching-stuck:'..(error or 'chest-full')},
color = {255, 0, 0},
}
end
end
function _M:empty_inserters()
local target = self:get_chest_inventory()
for _, inserter in pairs(self.assembler.surface.find_entities_filtered {
area = util.area(self.assembler.prototype.selection_box):expand(config.INSERTER_SEARCH_RADIUS) + self.assembler.position,
type = 'inserter',
}) do
if inserter.drop_target == self.assembler then
local stack = inserter.held_stack
if stack.valid_for_read and not self.settings.discard_items then
if not target then return false, 'no-chest'; end
local r = target.insert(stack)
if r < stack.count then
stack.count = stack.count - r
return false, 'chest-full'
end
stack.clear()
else stack.clear(); end
end
end
return true
end
function _M:find_assembler(assembler_to_ignore)
self.assembler = self.entity.surface.find_entities_filtered {
position = util.position(self.entity.position):shift(self.entity.direction, config.ASSEMBLER_DISTANCE),
type = 'assembling-machine',
}[1]
if self.assembler and (self.assembler == assembler_to_ignore or self.assembler.prototype.fixed_recipe) then
self.assembler = nil
end
if self.assembler then
self.inventories.assembler = {
output = self.assembler.get_inventory(defines.inventory.assembling_machine_output),
input = self.assembler.get_inventory(defines.inventory.assembling_machine_input),
modules = self.assembler.get_inventory(defines.inventory.assembling_machine_modules),
}
else self.inventories.assembler = {}; end
end
function _M:find_chest(chest_to_ignore)
local direction = util.direction(self.entity.direction):rotate(CHEST_DIRECTIONS[self.settings.chest_position])
self.chest = self.entity.surface.find_entities_filtered {
position = util.position(self.entity.position):shift(direction, config.CHEST_DISTANCE),
type = {'container', 'logistic-container'},
}[1]
if self.chest == chest_to_ignore then self.chest = nil; end
self.inventories.chest = self.chest and self.chest.get_inventory(defines.inventory.chest)
end
function _M:get_chest_inventory()
local inventory = self.inventories.chest
if not inventory or inventory.valid then return inventory; end
self:find_chest()
return self.inventories.chest
end
function _M:update_inner_positions()
settings_parser.move_entity(self.entity, self.module_chest.position)
self.module_chest.teleport(self.entity.position)
end
function _M:copy(source)
self.inventories.module_chest.set_bar(source.inventories.module_chest.get_bar())
end
return _M