345 lines
9.3 KiB
Lua

--[[
Wires can be placed in the following ways:
1) player clicks with a green-wire or red-wire
2) construction robot revives a ghost
3) a mod script (e.g. Nanobots) revives a ghost
4) a player clicks with a blueprint over an existing entity
Relevant events:
on_selected_entity_changed: before 1
on_player_cursor_stack_changed: after 1
on_robot_built_entity: 2
on_built_entity: 3
on_pre_build: 4
on_tick: after 4
]]
local M = {}
local blueprint = require "lualib.blueprint"
local event = require "lualib.event"
local util = require "lualib.util"
M.on_wire_added = script.generate_event_name()
M.on_wire_removed = script.generate_event_name()
-- how often to poll circuit network connections when the player is holding wire over an entity
local POLL_INTERVAL = 15
local monitored_players
--[[
CCD === CircuitConnectionDefinition
selected_ccd_set_for[player_index] = {
[ccd_key] = {
wire = ...,
target_entity = ...,
source_circuit_id = ...,
target_circuit_id = ...
},
...
}
]]
local selected_ccd_set_for
local function ccd_key(ccd)
--do return ccd.wire.."-"..ccd.source_circuit_id.."-"..ccd.target_circuit_id.."-"..ccd.target_entity.unit_number end
return (ccd.wire - 2)
+ (ccd.source_circuit_id - 1) * 2
+ (ccd.target_circuit_id - 1) * 4
+ ccd.target_entity.unit_number * 8
end
local function ccd_set(entity)
local ccds = entity.circuit_connection_definitions
if not ccds then
return {}
end
local out = {}
for i=1,#ccds do
out[ccd_key(ccds[i])] = ccds[i]
end
return out
end
local function diff_sets(old, new)
local removed = {}
for old_key, ccd in pairs(old) do
if not new[old_key] then
removed[#removed+1] = ccd
end
end
local added = {}
for new_key, ccd in pairs(new) do
if not old[new_key] then
added[#added+1] = ccd
end
end
return removed, added
end
local function raise_on_wire_added(entity, ccd)
local ev = {
entity = entity,
wire = ccd.wire,
target_entity = ccd.target_entity,
source_circuit_id = ccd.source_circuit_id,
target_circuit_id = ccd.target_circuit_id,
}
script.raise_event(M.on_wire_added, ev)
end
local function raise_on_wire_removed(entity, ccd)
local ev = {
entity = entity,
wire = ccd.wire,
target_entity = ccd.target_entity,
source_circuit_id = ccd.source_circuit_id,
target_circuit_id = ccd.target_circuit_id,
}
script.raise_event(M.on_wire_removed, ev)
end
local function check_for_circuit_changes(entity, old, new)
if not old or not new then
return
end
local removed, added = diff_sets(old, new)
for _, ccd in ipairs(removed) do
raise_on_wire_removed(entity, ccd)
end
for _, ccd in ipairs(added) do
raise_on_wire_added(entity, ccd)
end
end
local function check_selection_for_player(player_index)
local selected = game.players[player_index].selected
if selected and selected.valid then
local new = ccd_set(selected)
check_for_circuit_changes(selected, selected_ccd_set_for[player_index], new)
selected_ccd_set_for[player_index] = new
end
end
local function check_selection_for_all(ev)
if ev.tick % POLL_INTERVAL ~= 0 then
return
end
-- check only players who we believe to have a selected entity
for player_index in pairs(selected_ccd_set_for) do
check_selection_for_player(player_index)
end
end
local function start_monitoring_selected_entity(player_index)
local selected = game.players[player_index].selected
if selected then
selected_ccd_set_for[player_index] = ccd_set(selected)
event.register(defines.events.on_tick, check_selection_for_all)
return
end
selected_ccd_set_for[player_index] = nil
if not next(selected_ccd_set_for) then
event.unregister(defines.events.on_tick, check_selection_for_all)
end
end
local function on_selected_entity_changed(ev)
local player_index = ev.player_index
if not monitored_players[player_index] then
return
end
if ev.last_entity then
local new = ccd_set(ev.last_entity)
check_for_circuit_changes(ev.last_entity, selected_ccd_set_for[player_index], new)
end
start_monitoring_selected_entity(player_index)
end
local function stop_monitoring_player_selection(player_index)
-- one last check since we will no longer be monitoring this player's selection
check_selection_for_player(player_index)
monitored_players[player_index] = nil
if not next(monitored_players) then
event.unregister(defines.events.on_selected_entity_changed, on_selected_entity_changed)
end
selected_ccd_set_for[player_index] = nil
if not next(selected_ccd_set_for) then
event.unregister(defines.events.on_tick, check_selection_for_all)
end
end
local function start_monitoring_player_selection(player_index)
monitored_players[player_index] = true
start_monitoring_selected_entity(player_index)
event.register(defines.events.on_selected_entity_changed, on_selected_entity_changed)
end
local function on_player_cursor_stack_changed(ev)
local player_index = ev.player_index
local cursor_stack = game.players[player_index].cursor_stack
if cursor_stack.valid_for_read then
local name = cursor_stack.name
if name == "red-wire" or name == "green-wire" then
if monitored_players[player_index] then
-- already monitoring, probably placed a wire
check_selection_for_player(player_index)
else
start_monitoring_player_selection(player_index)
end
return
end
end
stop_monitoring_player_selection(player_index)
end
local function on_built_entity(ev)
local entity = ev.created_entity or ev.entity or ev.destination
if not entity.valid then return end
local ccds = entity.circuit_connection_definitions or {}
for _, ccd in ipairs(ccds) do
raise_on_wire_added(entity, ccd)
end
end
local function on_entity_mined(ev)
local entity = ev.entity
if entity.valid then
for _, ccd in ipairs(entity.circuit_connection_definitions or {}) do
raise_on_wire_removed(entity, ccd)
end
end
end
local bp_overplace
local function check_after_blueprint_placed()
for unit_number, data in pairs(bp_overplace) do
local entity = data.entity
-- any of the entities may have become invalid due to other scripting
if entity.valid then
local new = ccd_set(entity)
check_for_circuit_changes(entity, data.before_ccd_set, new)
end
bp_overplace[unit_number] = nil
end
event.unregister(defines.events.on_tick, check_after_blueprint_placed)
end
local function setup_after_blueprint_placed(preexisting_entities)
for i=1,#preexisting_entities do
local entity = preexisting_entities[i]
if entity.unit_number then
bp_overplace[entity.unit_number] = {
entity = entity,
before_ccd_set = ccd_set(entity),
}
end
end
event.register(defines.events.on_tick, check_after_blueprint_placed)
end
local function on_pre_build(ev)
local player = game.players[ev.player_index]
local bp_entities = player.get_blueprint_entities()
if not bp_entities or not next(bp_entities) then return end
local bp = player.cursor_stack
local bp_area = blueprint.bounding_box(bp_entities)
local surface_area = util.expand_box(
util.move_box(
util.rotate_box(bp_area, ev.direction),
ev.position
),
1
)
local preexisting_entities = player.surface.find_entities(surface_area)
-- check again at the end of this tick, after blueprint has been placed
setup_after_blueprint_placed(preexisting_entities)
end
function M.on_init()
global.onwireplaced = {
monitored_players = {},
selected_ccd_set_for = {},
bp_overplace = {},
}
M.on_load()
end
function M.on_load()
if not global.onwireplaced then
return -- expect on_configuration_changed to be called
end
if global.onwireplaced.monitored_players then
monitored_players = global.onwireplaced.monitored_players
if next(monitored_players) then
event.register(defines.events.on_selected_entity_changed, on_selected_entity_changed)
end
end
if global.onwireplaced.selected_ccd_set_for then
selected_ccd_set_for = global.onwireplaced.selected_ccd_set_for
if next(selected_ccd_set_for) then
event.register(defines.events.on_tick, check_selection_for_all)
end
end
if global.onwireplaced.bp_overplace then
bp_overplace = global.onwireplaced.bp_overplace
if next(bp_overplace) then
event.register(defines.events.on_tick, check_after_blueprint_placed)
end
end
event.register(defines.events.on_player_cursor_stack_changed, on_player_cursor_stack_changed)
event.register(
{
defines.events.on_built_entity,
defines.events.on_entity_cloned,
defines.events.on_robot_built_entity,
defines.events.script_raised_built,
defines.events.script_raised_revive,
},
on_built_entity
)
event.register(
{
defines.events.on_entity_died,
defines.events.on_player_mined_entity,
defines.events.on_robot_mined_entity,
defines.events.script_raised_destroy,
},
on_entity_mined
)
event.register(defines.events.on_pre_build, on_pre_build)
end
function M.on_configuration_changed()
if not global.onwireplaced then
global.onwireplaced = {
monitored_players = global.monitored_players or {},
selected_ccd_set_for = global.selected_ccd_set_for or {},
bp_overplace = {},
}
global.monitored_players = nil
global.selected_ccd_set_for = nil
end
M.on_load()
end
return M