1548 lines
56 KiB
Lua
1548 lines
56 KiB
Lua
-- Laser "physics". This file contains the code that propagates
|
||
-- the lasers, updates the laser nodes and the map.
|
||
|
||
local S = minetest.get_translator("lzr_laser")
|
||
|
||
-- Max. number of steps in the laser travel algorithm
|
||
local MAX_LASER_ITERATIONS = 10000
|
||
|
||
-- Max. number of nodes a laser can extend out of level bounds
|
||
local MAX_LASERS_OUT_OF_BOUNDS_DISTANCE = 100
|
||
|
||
-- How long a barricade burns, in seconds
|
||
local BARRICADE_BURN_TIME = 1.0
|
||
|
||
-- How long the bomb fuse burns, in seconds,
|
||
-- when ignited directly.
|
||
local BOMB_BURN_TIME = 2.0
|
||
-- How long the bomb takes to explode
|
||
-- when it was caught up in an explosion,
|
||
-- in seconds
|
||
local BOMB_BURN_TIME_QUICK = 0.2
|
||
|
||
local destroy_events = {}
|
||
|
||
local out_of_bounds_lasers = {}
|
||
local out_of_bounds_destroyeds = {}
|
||
|
||
local recently_touched_receivers = {}
|
||
|
||
-- If true, the laser simulation is frozen. While frozen,
|
||
-- lasers won't automatically update on map changes.
|
||
local lasers_frozen = false
|
||
|
||
-- Calculate all node positions of an out-of-bounds laser starting at `pos`,
|
||
-- direction `dir` and colorcode `colorcode`.
|
||
-- An out-of-bounds laser will extend up to MAX_LASERS_OUT_OF_BOUNDS_DISTANCE
|
||
-- nodes.
|
||
-- Returns <positions>, <barrier_pos>, <destroyed_node_positions>, <laser node>
|
||
function lzr_laser.travel_laser_out_of_bounds(pos, dir, colorcode)
|
||
local posses = {}
|
||
local barrier_pos
|
||
local destroyeds = {}
|
||
local laser_node
|
||
local dirs = lzr_laser.vector_and_color_to_dirs(dir, colorcode)
|
||
local dirstring = lzr_laser.dirs_to_dirstring(dirs)
|
||
local laser_node
|
||
if colorcode == 0 then
|
||
laser_node = { name = "air" }
|
||
else
|
||
laser_node = { name = "lzr_laser:laser_"..dirstring }
|
||
end
|
||
|
||
local i = 0
|
||
local first_pos
|
||
while i < MAX_LASERS_OUT_OF_BOUNDS_DISTANCE do
|
||
if not first_pos then
|
||
first_pos = pos
|
||
end
|
||
local node = minetest.get_node(pos)
|
||
local ld = minetest.get_item_group(node.name, "laser_destroys")
|
||
local la = minetest.get_item_group(node.name, "laser")
|
||
|
||
-- First block in path is a barrier
|
||
if i == 0 and (minetest.get_item_group(node.name, "barrier") > 0 or minetest.get_item_group(node.name, "rain_membrane") > 0) then
|
||
barrier_pos = pos
|
||
-- Laser through air or destroyable block or existing laser
|
||
elseif node.name == "air" or ld == 1 or la ~= 0 then
|
||
table.insert(posses, pos)
|
||
if ld == 1 then
|
||
table.insert(destroyeds, { pos = pos, node = node, start_pos = first_pos })
|
||
end
|
||
else
|
||
break
|
||
end
|
||
pos = vector.add(pos, dir)
|
||
i = i + 1
|
||
end
|
||
return posses, barrier_pos, destroyeds, laser_node
|
||
end
|
||
|
||
-- This propagates a *single laser* by a *single step* and checks what
|
||
-- to do with the next node. This will either add the laser, stop the
|
||
-- laser propagation (due to collision), or do a special event in case we hit a
|
||
-- laser block (mirror, crystal, etc.).
|
||
-- This function only works on VoxelManip data!
|
||
-- Parameters:
|
||
-- * `pos`: start position
|
||
-- * `dir`: laser direction
|
||
-- * `colorcode`: laser colorcode
|
||
-- * `varea`: VoxelArea for the VoxelManip workable area
|
||
-- * `vdata`: VoxelManip data table gotten with `get_data`
|
||
-- * `vdata_p2`: VoxelManip data table, but for param2
|
||
-- * `emit_state`: Table that contains more complex info about the laser state (call-by-reference)
|
||
--
|
||
-- Returns a list of the *next* laser positions and direction, each entry is in format:
|
||
-- { pos, dir, colorcode }
|
||
-- This indicates where to spawn the next lasers (pos), where they are headed
|
||
-- towards (dir) and what color they are (colorcode).
|
||
-- Some laser blocks may spawn multiple lasers (crystal, beam spliter),
|
||
-- this is why it must be a list.
|
||
-- If the laser terminates (e.g. due to collision), returns false instead.
|
||
function lzr_laser.add_laser(pos, dir, colorcode, varea, vdata, vdata_p2, emit_state)
|
||
local lminpos, lmaxpos = lzr_world.get_level_bounds()
|
||
-- Check if laser is going outside the level bounds
|
||
if pos.x < lminpos.x or pos.x > lmaxpos.x or
|
||
pos.y < lminpos.y or pos.y > lmaxpos.y or
|
||
pos.z < lminpos.z or pos.z > lmaxpos.z then
|
||
return { "laser_out_of_bounds", pos, dir, colorcode }
|
||
end
|
||
local vi = varea:indexp(pos)
|
||
local content_id = vdata[vi]
|
||
local param2 = vdata_p2[vi]
|
||
local nodename = minetest.get_name_from_content_id(content_id)
|
||
local ld = minetest.get_item_group(nodename, "laser_destroys")
|
||
-- Laser through air or destroyable block
|
||
if content_id == minetest.CONTENT_AIR or ld == 1 then
|
||
local dirs = lzr_laser.vector_and_color_to_dirs(dir, colorcode)
|
||
local dirstring = lzr_laser.dirs_to_dirstring(dirs)
|
||
vdata[vi] = minetest.get_content_id("lzr_laser:laser_"..dirstring)
|
||
if ld == 1 then
|
||
table.insert(emit_state.destroy_cache, {pos=pos, nodename=nodename})
|
||
end
|
||
-- Just advance straight ahead
|
||
pos = vector.add(pos, dir)
|
||
return { "laser", {pos, dir, colorcode}}
|
||
-- Burning block
|
||
elseif ld == 2 or ld == 3 then
|
||
local def = minetest.registered_nodes[nodename]
|
||
local active = def._lzr_active
|
||
if active then
|
||
local burn = false
|
||
if ld == 2 then
|
||
burn = true
|
||
-- Gets ignited from top only (bomb)
|
||
elseif ld == 3 then
|
||
local top_dir = lzr_laser.get_top_dir(param2)
|
||
local inverted_dir = vector.multiply(dir, -1)
|
||
if vector.equals(inverted_dir, top_dir) then
|
||
burn = true
|
||
end
|
||
end
|
||
-- Only burn in-game (for editor convenience)
|
||
local gs = lzr_gamestate.get_state()
|
||
if burn and (gs == lzr_gamestate.LEVEL or gs == lzr_gamestate.LEVEL_TEST) then
|
||
vdata[vi] = minetest.get_content_id(active)
|
||
table.insert(emit_state.burning_cache, vi)
|
||
end
|
||
else
|
||
minetest.log("error", "[lzr_laser] Node definition of "..nodename.." has laser_destroys="..ld.." but no _lzr_active")
|
||
end
|
||
-- Laser collides
|
||
return false
|
||
-- Laser through laser (laser intersection)
|
||
elseif minetest.get_item_group(nodename, "laser") > 0 then
|
||
local laser_group = minetest.get_item_group(nodename, "laser")
|
||
local dirstring_old = lzr_laser.laser_group_to_dirstring(laser_group)
|
||
|
||
local dirs_new = lzr_laser.vector_and_color_to_dirs(dir, colorcode)
|
||
local dirstring_new = lzr_laser.dirs_to_dirstring(dirs_new)
|
||
|
||
local place_dirstring = lzr_laser.dirstring_or(dirstring_old, dirstring_new)
|
||
vdata[vi] = minetest.get_content_id("lzr_laser:laser_"..place_dirstring)
|
||
-- Advance straight ahead
|
||
pos = vector.add(pos, dir)
|
||
return { "laser", {pos, dir, colorcode}}
|
||
-- Laser through skull
|
||
elseif minetest.get_item_group(nodename, "skull_shy") > 0 or minetest.get_item_group(nodename, "skull_cursed") > 0 then
|
||
local def = minetest.registered_nodes[nodename]
|
||
local active = def._lzr_active
|
||
if active then
|
||
-- Activate skull node
|
||
vdata[vi] = minetest.get_content_id(def._lzr_active)
|
||
end
|
||
pos = vector.add(pos, dir)
|
||
return { "laser", {pos, dir, colorcode}}
|
||
-- Mirror laser
|
||
elseif minetest.get_item_group(nodename, "mirror") > 0 then
|
||
local mirror_dir = lzr_laser.get_mirrored_laser_dir(nodename, param2, dir)
|
||
if mirror_dir then
|
||
local def = minetest.registered_nodes[nodename]
|
||
local active = def._lzr_active
|
||
if active then
|
||
-- Activate mirror node and mix color
|
||
local old_colorcode = minetest.get_item_group(nodename, "laser_block_color")
|
||
local new_colorcode = bit.bor(colorcode, old_colorcode)
|
||
vdata[vi] = minetest.get_content_id(active.."_"..new_colorcode)
|
||
end
|
||
-- Set new pos and dir after calculating mirror direction
|
||
pos = vector.add(pos, mirror_dir)
|
||
dir = mirror_dir
|
||
return { "laser", {pos, dir, colorcode}}
|
||
else
|
||
return false
|
||
end
|
||
-- Mirror laser (double mirror)
|
||
elseif minetest.get_item_group(nodename, "double_mirror") > 0 then
|
||
local mirror_dir, _, mirror_side = lzr_laser.get_mirrored_laser_dir(nodename, param2, dir)
|
||
if mirror_dir then
|
||
local def = minetest.registered_nodes[nodename]
|
||
local state = def._lzr_double_mirror_state
|
||
if not state then
|
||
minetest.log("error", "[lzr_laser] Double mirror node '"..nodename.."' does not have _lzr_double_mirror_state!")
|
||
return false
|
||
end
|
||
-- Combine double mirror state (laser on frontside or
|
||
-- backside of the mirror?)
|
||
if mirror_side == true then
|
||
state = lzr_laser.dirstring_or(state, "0"..colorcode)
|
||
else
|
||
state = lzr_laser.dirstring_or(state, colorcode.."0")
|
||
end
|
||
|
||
local is_takable = minetest.get_item_group(nodename, "takable") ~= 0
|
||
local is_rotatable = minetest.get_item_group(nodename, "rotatable") == 1
|
||
local active = "lzr_laser:double_mirror_"..state
|
||
if is_takable then
|
||
active = active.."_takable"
|
||
elseif is_rotatable then
|
||
active = active.."_rotatable"
|
||
else
|
||
active = active.."_fixed"
|
||
end
|
||
|
||
-- Set new double mirror node state
|
||
vdata[vi] = minetest.get_content_id(active)
|
||
|
||
-- Set new pos and dir after calculating mirror direction
|
||
pos = vector.add(pos, mirror_dir)
|
||
dir = mirror_dir
|
||
return { "laser", {pos, dir, colorcode}}
|
||
else
|
||
-- Laser came from the wrong side so it ends here
|
||
return false
|
||
end
|
||
|
||
local mirror_dir = lzr_laser.get_mirrored_laser_dir(nodename, param2, dir)
|
||
if mirror_dir then
|
||
local def = minetest.registered_nodes[nodename]
|
||
local active = def._lzr_active
|
||
if active then
|
||
-- Activate mirror node and mix color
|
||
local old_colorcode = minetest.get_item_group(nodename, "laser_block_color")
|
||
local new_colorcode = bit.bor(colorcode, old_colorcode)
|
||
vdata[vi] = minetest.get_content_id(active.."_"..new_colorcode)
|
||
end
|
||
-- Set new pos and dir after calculating mirror direction
|
||
pos = vector.add(pos, mirror_dir)
|
||
dir = mirror_dir
|
||
return { "laser", {pos, dir, colorcode}}
|
||
else
|
||
return false
|
||
end
|
||
|
||
-- Mirror and split laser
|
||
elseif minetest.get_item_group(nodename, "transmissive_mirror") > 0 then
|
||
local mirror_dir, mirror_ingoing = lzr_laser.get_mirrored_laser_dir(nodename, param2, dir)
|
||
if mirror_dir then
|
||
local def = minetest.registered_nodes[nodename]
|
||
local state = def._lzr_transmissive_mirror_state
|
||
if not state then
|
||
minetest.log("error", "[lzr_laser] Transmissive mirror node '"..nodename.."' does not have _lzr_transmissive_mirror_state!")
|
||
return false
|
||
end
|
||
-- Combine internal laser state
|
||
if mirror_ingoing == true then
|
||
-- Combine current state with 0X (X = incoming colorcode)
|
||
state = lzr_laser.dirstring_or(state, "0"..colorcode)
|
||
else
|
||
-- Combine current state with X0 (X = incoming colorcode)
|
||
state = lzr_laser.dirstring_or(state, colorcode.."0")
|
||
end
|
||
|
||
local is_takable = minetest.get_item_group(nodename, "takable") ~= 0
|
||
local is_rotatable = minetest.get_item_group(nodename, "rotatable") == 1
|
||
local active = "lzr_laser:transmissive_mirror_"..state
|
||
if is_takable then
|
||
active = active.."_takable"
|
||
elseif is_rotatable then
|
||
active = active.."_rotatable"
|
||
else
|
||
active = active.."_fixed"
|
||
end
|
||
|
||
-- Set new node state
|
||
vdata[vi] = minetest.get_content_id(active)
|
||
|
||
-- Report new laser positions and directions.
|
||
-- This always will report 2 new lasers.
|
||
-- The laser goes right through ...
|
||
local pos_straight = vector.add(pos, dir)
|
||
local dir_straight = dir
|
||
-- ... and it is also deflected
|
||
local pos_mirrored = vector.add(pos, mirror_dir)
|
||
local dir_mirrored = mirror_dir
|
||
return {
|
||
"laser",
|
||
-- The laser that went straight through
|
||
{pos_straight, dir_straight, colorcode},
|
||
-- The mirrored laser
|
||
{pos_mirrored, dir_mirrored, colorcode}
|
||
}
|
||
else
|
||
-- Laser came from the wrong side so it ends here
|
||
return false
|
||
end
|
||
-- Crystal: Spread laser to all directions
|
||
elseif minetest.get_item_group(nodename, "crystal") > 0 then
|
||
local def = minetest.registered_nodes[nodename]
|
||
local active = def._lzr_active
|
||
if active then
|
||
-- Activate node and mix color
|
||
local old_colorcode = minetest.get_item_group(nodename, "laser_block_color")
|
||
local new_colorcode = bit.bor(colorcode, old_colorcode)
|
||
|
||
if minetest.get_item_group(nodename, "laser_block_color") == new_colorcode then
|
||
return false
|
||
end
|
||
vdata[vi] = minetest.get_content_id(active.."_"..new_colorcode)
|
||
end
|
||
|
||
-- Set dirs to spread laser towards
|
||
local dirs = {
|
||
vector.new(0, -1, 0),
|
||
vector.new(0, 1, 0),
|
||
vector.new(-1, 0, 0),
|
||
vector.new(1, 0, 0),
|
||
vector.new(0, 0, -1),
|
||
vector.new(0, 0, 1),
|
||
}
|
||
-- Don't spread to the direction we came from!
|
||
local fromdir = vector.multiply(dir, -1)
|
||
for d=1, #dirs do
|
||
if vector.equals(dirs[d], fromdir) then
|
||
table.remove(dirs, d)
|
||
break
|
||
end
|
||
end
|
||
local output = { "laser" }
|
||
for d=1, #dirs do
|
||
table.insert(output, { vector.add(pos, dirs[d]), dirs[d], colorcode })
|
||
end
|
||
return output
|
||
-- Mixer: Mix laser colors
|
||
elseif minetest.get_item_group(nodename, "mixer") > 0 then
|
||
local output_dir = lzr_laser.get_front_dir(param2)
|
||
local input_dir_l, input_dir_r = lzr_laser.get_mixer_input_dirs(param2)
|
||
if (input_dir_l and vector.equals(dir, input_dir_l)) or (input_dir_r and vector.equals(dir, input_dir_r)) then
|
||
local def = minetest.registered_nodes[nodename]
|
||
|
||
local active = def._lzr_active
|
||
if active then
|
||
-- Activate node and mix output color of node
|
||
local old_colorcode = minetest.get_item_group(nodename, "laser_block_color")
|
||
local new_colorcode = bit.bor(colorcode, old_colorcode)
|
||
-- Stop laser propagation if we already output this laser color
|
||
-- to break potential infinite loops.
|
||
if old_colorcode == new_colorcode then
|
||
return false
|
||
end
|
||
vdata[vi] = minetest.get_content_id(active.."_"..new_colorcode)
|
||
end
|
||
|
||
--[[ NOTE: The mixer does NOT actually mix the laser color,
|
||
it just propagates the input laser to the
|
||
output with the color unchanged. Since the mixer
|
||
has multiple inputs, if lasers go in into all inputs,
|
||
they will overlap behind the output (see the behavior
|
||
on laser-to-laser collision). So internally,
|
||
*all* input lasers will travel out of the mixer as they
|
||
came in separately, but they will overlap as a result of the
|
||
laser propagation algorithm. ]]
|
||
|
||
local output_pos = vector.add(pos, output_dir)
|
||
return {
|
||
"laser",
|
||
{output_pos, output_dir, colorcode},
|
||
}
|
||
else
|
||
-- Laser came from the wrong side so it ends here
|
||
return false
|
||
end
|
||
|
||
-- Detector
|
||
elseif minetest.get_item_group(nodename, "detector") > 0 then
|
||
local detected = lzr_laser.check_detector_input(nodename, param2, dir)
|
||
if detected then
|
||
local detector_color = minetest.get_item_group(nodename, "detector_color")
|
||
-- Colorless detector always activates with any laser color
|
||
if detector_color == 0 then
|
||
local def = minetest.registered_nodes[nodename]
|
||
local active = def._lzr_active
|
||
if active then
|
||
-- Activate node
|
||
vdata[vi] = minetest.get_content_id(active)
|
||
return false
|
||
end
|
||
-- Colored detector: We have to report this to the calling function
|
||
else
|
||
return { "detected", pos, colorcode }
|
||
end
|
||
end
|
||
-- Laser ends here
|
||
return false
|
||
-- Hollow barrel
|
||
elseif minetest.get_item_group(nodename, "hollow_barrel") > 0 then
|
||
-- Laser can through the hollow part
|
||
local axis = lzr_laser.get_barrel_axis(param2)
|
||
if (dir.x ~= 0 and axis == "x") or
|
||
(dir.y ~= 0 and axis == "y") or
|
||
(dir.z ~= 0 and axis == "z") then
|
||
local def = minetest.registered_nodes[nodename]
|
||
local active = def._lzr_active
|
||
if active then
|
||
-- Combine color
|
||
local old_colorcode = minetest.get_item_group(nodename, "laser_block_color")
|
||
local new_colorcode = bit.bor(colorcode, old_colorcode)
|
||
-- Activate barrel node
|
||
vdata[vi] = minetest.get_content_id(active.."_"..new_colorcode)
|
||
end
|
||
pos = vector.add(pos, dir)
|
||
return { "laser", {pos, dir, colorcode}}
|
||
else
|
||
return false
|
||
end
|
||
-- Pane
|
||
elseif minetest.get_item_group(nodename, "pane") > 0 then
|
||
-- Laser can through the pane
|
||
local axis = lzr_laser.get_pane_axis(param2)
|
||
if (dir.x ~= 0 and axis == "x") or
|
||
(dir.y ~= 0 and axis == "y") or
|
||
(dir.z ~= 0 and axis == "z") then
|
||
local def = minetest.registered_nodes[nodename]
|
||
local active = def._lzr_active
|
||
if active then
|
||
-- Combine color
|
||
local old_colorcode = minetest.get_item_group(nodename, "laser_block_color")
|
||
local new_colorcode = bit.bor(colorcode, old_colorcode)
|
||
-- Activate pane node
|
||
vdata[vi] = minetest.get_content_id(active.."_"..new_colorcode)
|
||
end
|
||
pos = vector.add(pos, dir)
|
||
return { "laser", {pos, dir, colorcode}}
|
||
else
|
||
return false
|
||
end
|
||
-- An open empty chest, or a bottom slab
|
||
elseif minetest.get_item_group(nodename, "chest_open") > 0 or minetest.get_item_group(nodename, "slab") == 1 then
|
||
-- Laser can go into it from above
|
||
-- (this also works for slab because it cannot rotate)
|
||
if dir.y < 0 then
|
||
local def = minetest.registered_nodes[nodename]
|
||
local active = def._lzr_active
|
||
if active then
|
||
-- Laser beam from above goes into chest/slab
|
||
vdata[vi] = minetest.get_content_id(active.."_"..colorcode)
|
||
end
|
||
end
|
||
-- Laser ends here
|
||
return false
|
||
|
||
-- Palm leaves or half cabinet or top slab
|
||
elseif minetest.get_item_group(nodename, "palm_leaves") > 0 or minetest.get_item_group(nodename, "cabinet_half") > 0 or minetest.get_item_group(nodename, "slab") == 2 then
|
||
-- Laser can go into it from below
|
||
if dir.y > 0 then
|
||
local def = minetest.registered_nodes[nodename]
|
||
local active = def._lzr_active
|
||
if active then
|
||
-- Laser beam from below
|
||
vdata[vi] = minetest.get_content_id(active.."_"..colorcode)
|
||
end
|
||
end
|
||
-- Laser ends here
|
||
return false
|
||
|
||
-- Ship's wheel
|
||
elseif minetest.get_item_group(nodename, "ships_wheel") > 0 then
|
||
local front_dir = lzr_laser.get_front_dir(param2)
|
||
-- Laser can go into it from the front
|
||
if vector.equals(front_dir, vector.multiply(dir, -1)) then
|
||
local def = minetest.registered_nodes[nodename]
|
||
local active = def._lzr_active
|
||
if active then
|
||
-- Laser beam from the front
|
||
vdata[vi] = minetest.get_content_id(active.."_"..colorcode)
|
||
end
|
||
end
|
||
-- Laser ends here
|
||
return false
|
||
|
||
-- Anything else terminates the laser
|
||
else
|
||
return false
|
||
end
|
||
end
|
||
|
||
-- Emit a laser from an emitter and starts laser propagation.
|
||
-- * `pos`: position of emitter
|
||
-- * `colorcode`: laser colorcode
|
||
-- * `varea`, `vdata`, `vdata_p2`: See `lzr_laser.add_laser`
|
||
-- * `emit_state`: Table that contains more complex info about the laser state (call-by-reference)
|
||
-- * `max_iterations`: Maximum number of allowed iterations before halting (optional)
|
||
function lzr_laser.emit_laser(pos, colorcode, varea, vdata, vdata_p2, emit_state, max_iterations)
|
||
local vi = varea:indexp(pos)
|
||
local content_id = vdata[vi]
|
||
local nodename = minetest.get_name_from_content_id(content_id)
|
||
if minetest.get_item_group(nodename, "emitter") == 0 then
|
||
minetest.log("error", "[lzr_laser] lzr_laser.emit_laser was called at invalid pos!")
|
||
return false
|
||
end
|
||
local param2 = vdata_p2[vi]
|
||
|
||
local dir = minetest.facedir_to_dir(param2)
|
||
dir = vector.multiply(dir, -1)
|
||
local i_pos = vector.add(pos, dir)
|
||
lzr_laser.travel_laser(i_pos, dir, colorcode, varea, vdata, vdata_p2, emit_state, max_iterations)
|
||
end
|
||
|
||
-- Spawns and propagates a single laser from `pos` step-by-step
|
||
-- until either all laser beams that have been created during
|
||
-- travel have terminated, or the algorithm took too many
|
||
-- iterations (which is probably an error pointing to an infinite
|
||
-- laser loop; or the map is just very complex).
|
||
-- * `pos`: position of laser
|
||
-- * `dir`: direction towards the laser will travel to initially
|
||
-- * `colorcode`: laser colorcode
|
||
-- * `varea`, `vdata`, `vdata_p2`: See `lzr_laser.add_laser`
|
||
-- * `emit_state`: Table that contains more complex info about the laser state (call-by-reference)
|
||
-- * `max_iterations`: Maximum number of allowed iterations before halting (optional)
|
||
function lzr_laser.travel_laser(pos, dir, colorcode, varea, vdata, vdata_p2, emit_state, max_iterations)
|
||
local i_pos = table.copy(pos)
|
||
-- This is a list of all currently "travelling lasers" that
|
||
-- spawned from the initial laser. Each travelling laser
|
||
-- has a position, direction and colorcode.
|
||
-- This list initializes with a single position, from where we start.
|
||
-- Each time add_laser is called, the travelling laser is removed, but
|
||
-- any new travelling lasers returned from add_laser will be added to the
|
||
-- list.
|
||
-- This essentially is a breadth-first search.
|
||
local next_lasers = {{i_pos, dir, colorcode}}
|
||
|
||
local custom_max_iterations = false
|
||
if not max_iterations then
|
||
max_iterations = MAX_LASER_ITERATIONS
|
||
else
|
||
custom_max_iterations = true
|
||
end
|
||
|
||
local i = 0
|
||
while true do
|
||
i = i + 1
|
||
-- Halt execution for very long loops to prevent freezing the game
|
||
if i > max_iterations then
|
||
if not custom_max_iterations then
|
||
minetest.log("error", "[lzr_laser] lzr_laser.travel_laser aborted (too many iterations!)")
|
||
end
|
||
for n=1, #next_lasers do
|
||
local next_laser = next_lasers[n]
|
||
local tex
|
||
if n == 1 then
|
||
tex = "lzr_laser_laser_end_debug.png"
|
||
else
|
||
tex = "lzr_laser_laser_end_debug_extra.png"
|
||
end
|
||
minetest.add_particle({
|
||
pos = next_laser[1],
|
||
glow = minetest.LIGHT_MAX,
|
||
size = 2,
|
||
texture = tex,
|
||
expirationtime = 1,
|
||
})
|
||
end
|
||
break
|
||
end
|
||
|
||
-- Get next laser and propagate it by one step
|
||
local next_laser = next_lasers[1]
|
||
local add_laser_result = lzr_laser.add_laser(next_laser[1], next_laser[2], next_laser[3], varea, vdata, vdata_p2, emit_state)
|
||
|
||
table.remove(next_lasers, 1)
|
||
if add_laser_result ~= false then
|
||
local result_type = add_laser_result[1]
|
||
-- Propagate lasers
|
||
if result_type == "laser" then
|
||
for a=2, #add_laser_result do
|
||
table.insert(next_lasers, add_laser_result[a])
|
||
end
|
||
-- Laser reached out of bounds
|
||
elseif result_type == "laser_out_of_bounds" then
|
||
table.insert(emit_state.out_of_bounds, {pos=add_laser_result[2], dir=add_laser_result[3], colorcode=add_laser_result[4]})
|
||
-- Colored detector hit
|
||
elseif result_type == "detected" then
|
||
local poshash = minetest.hash_node_position(add_laser_result[2])
|
||
table.insert(emit_state.detections, {hash = poshash, colorcode = add_laser_result[3]})
|
||
end
|
||
end
|
||
-- When the table is empty, this means all travelling lasers have terminated. Success!
|
||
if #next_lasers == 0 then
|
||
break
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Remove all out-of-bounds lasers.
|
||
-- Since the starting positions and directions of out-of-bounds lasers are stored in a
|
||
-- variable, no arguments are needed.
|
||
-- NOTE: Unlike the other laser functions, this function acts on the map directly,
|
||
-- it does NOT work on a LuaVoxelManip.
|
||
-- This function MUST NOT be called in the middle of LuaVoxelManip operations.
|
||
function lzr_laser.clear_out_of_bounds_lasers(kept_lasers)
|
||
local clear_oob_lasers = {}
|
||
local clear_barriers = {}
|
||
local destroyed_lasers = {}
|
||
for hash, oob in pairs(out_of_bounds_lasers) do
|
||
local hash = minetest.hash_node_position(oob.pos)
|
||
if not kept_lasers or not kept_lasers[hash] then
|
||
local oob_lasers, barrier_pos = lzr_laser.travel_laser_out_of_bounds(oob.pos, oob.dir, oob.colorcode)
|
||
table.insert_all(clear_oob_lasers, oob_lasers)
|
||
destroyed_lasers[hash] = true
|
||
if barrier_pos then
|
||
table.insert(clear_barriers, barrier_pos)
|
||
end
|
||
out_of_bounds_lasers[hash] = nil
|
||
end
|
||
end
|
||
if #clear_oob_lasers > 0 then
|
||
minetest.bulk_set_node(clear_oob_lasers, { name = "air" })
|
||
end
|
||
for b=1, #clear_barriers do
|
||
local bpos = clear_barriers[b]
|
||
local node = minetest.get_node(bpos)
|
||
if minetest.get_item_group(node.name, "rain_membrane") > 0 then
|
||
minetest.set_node(bpos, { name = "lzr_core:rain_membrane" })
|
||
elseif minetest.get_item_group(node.name, "barrier") > 0 then
|
||
minetest.set_node(bpos, { name = "lzr_core:barrier" })
|
||
end
|
||
end
|
||
|
||
-- Restore 'laser_destroys=1' nodes (mostly plants)
|
||
-- after the lasers were cleared
|
||
local restoreds = {}
|
||
for d=1, #out_of_bounds_destroyeds do
|
||
local destr = out_of_bounds_destroyeds[d]
|
||
local hash = minetest.hash_node_position(destr.start_pos)
|
||
if destroyed_lasers[hash] then
|
||
minetest.set_node(destr.pos, destr.node)
|
||
table.insert(restoreds, d)
|
||
end
|
||
end
|
||
for r=#restoreds, 1, -1 do
|
||
table.remove(out_of_bounds_destroyeds, restoreds[r])
|
||
end
|
||
end
|
||
|
||
-- Remove all lasers in the given area and disable all laser blocks
|
||
-- (e.g. mirrors, detectors)
|
||
-- * pos1: Minimum position of area
|
||
-- * pos2: Maximum position of area
|
||
-- * varea, vdata: See lzr_laser.add_laser
|
||
function lzr_laser.clear_lasers_in_area(pos1, pos2, varea, vdata)
|
||
for z=pos1.z, pos2.z do
|
||
for y=pos1.y, pos2.y do
|
||
for x=pos1.x, pos2.x do
|
||
local vi = varea:indexp({x=x,y=y,z=z})
|
||
local cid = vdata[vi]
|
||
local nodename = minetest.get_name_from_content_id(cid)
|
||
if minetest.get_item_group(nodename, "laser") ~= 0 then
|
||
vdata[vi] = minetest.CONTENT_AIR
|
||
elseif minetest.get_item_group(nodename, "laser_block") ~= 0 then
|
||
local def = minetest.registered_nodes[nodename]
|
||
local is_ignored_node =
|
||
minetest.get_item_group(nodename, "emitter") > 0 or
|
||
minetest.get_item_group(nodename, "bomb") > 0 or
|
||
minetest.get_item_group(nodename, "barricade") > 0
|
||
if def and not is_ignored_node then
|
||
local inactive = def._lzr_inactive
|
||
if inactive then
|
||
vdata[vi] = minetest.get_content_id(inactive)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Emit lasers from all *active* emitters in area.
|
||
-- * pos1: Minimum position of area
|
||
-- * pos2: Maximum position of area
|
||
-- * ignore_emitters
|
||
-- * varea, vdata, vdata_p2: See lzr_laser.add_laser
|
||
-- * emit_state: Table that contains more complex info about the laser state (call-by-reference)
|
||
-- * max_iterations: Maximum number of allowed iterations before halting (optional)
|
||
function lzr_laser.emit_lasers_in_area(pos1, pos2, varea, vdata, vdata_p2, emit_state, max_iterations)
|
||
local emitters = minetest.find_nodes_in_area(pos1, pos2, {"group:emitter"})
|
||
for e=1, #emitters do
|
||
local epos = emitters[e]
|
||
local vi = varea:indexp(epos)
|
||
local emitter_cid = vdata[vi]
|
||
local emittername = minetest.get_name_from_content_id(emitter_cid)
|
||
local is_active = minetest.get_item_group(emittername, "emitter") == 2
|
||
local colorcode = minetest.get_item_group(emittername, "emitter_color")
|
||
if is_active and colorcode ~= 0 then
|
||
lzr_laser.emit_laser(emitters[e], colorcode, varea, vdata, vdata_p2, emit_state, max_iterations)
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Returns true if there are no unclaimed treasures remaining in area
|
||
function lzr_laser.check_treasures_in_area(pos1, pos2)
|
||
local closed_chests = minetest.find_nodes_in_area(pos1, pos2, {"group:chest_closed"})
|
||
return #closed_chests > 0
|
||
end
|
||
|
||
-- Returns the number of treasures found in current level in area
|
||
function lzr_laser.count_found_treasures(pos1, pos2)
|
||
return #minetest.find_nodes_in_area(pos1, pos2, {"group:chest_open_treasure"})
|
||
end
|
||
|
||
-- Returns true if current level is won
|
||
function lzr_laser.check_level_won()
|
||
local minpos, maxpos = lzr_world.get_level_bounds()
|
||
return not lzr_laser.check_treasures_in_area(minpos, maxpos)
|
||
end
|
||
|
||
-- Returns a table of all detector states in the given area,
|
||
-- indexed by the VoxelArea index of a VoxelManip and the value
|
||
-- being either true for active, false for inactive and nil
|
||
-- for any position without a detector.
|
||
local function get_detector_states_in_area(pos1, pos2, varea, vdata)
|
||
local states = {}
|
||
for vi=1, #vdata do
|
||
local cid = vdata[vi]
|
||
local nodename = minetest.get_name_from_content_id(cid)
|
||
local dstate = minetest.get_item_group(nodename, "detector")
|
||
if dstate == 1 then
|
||
states[vi] = false
|
||
elseif dstate == 2 then
|
||
states[vi] = true
|
||
end
|
||
end
|
||
return states
|
||
end
|
||
local function get_sender_states_in_area(pos1, pos2, varea, vdata)
|
||
local states = {}
|
||
for vi=1, #vdata do
|
||
local cid = vdata[vi]
|
||
local nodename = minetest.get_name_from_content_id(cid)
|
||
if minetest.get_item_group(nodename, "sender") ~= 0 then
|
||
local def = minetest.registered_nodes[nodename]
|
||
if def then
|
||
local groupname = def._lzr_element_group
|
||
if groupname then
|
||
local dstate = minetest.get_item_group(nodename, groupname)
|
||
if dstate == 1 then
|
||
states[vi] = false
|
||
elseif dstate == 2 then
|
||
states[vi] = true
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
return states
|
||
end
|
||
|
||
-- Recalculate all lasers in area.
|
||
-- * pos1: Minimum position of area
|
||
-- * pos2: Maximum position of area
|
||
-- * force_update: If true, will recalculate lasers even if lasers are frozen (default: false)
|
||
-- * clear_first: If true, will clear the lasers first (default: true)
|
||
-- * max_iterations: Maximum number of allowed iterations before halting (optional)
|
||
-- * extra_state: Table that contains more complex info about the laser state (optional)
|
||
local function laser_update(pos1, pos2, force_update, clear_first, max_iterations, extra_state)
|
||
if lzr_laser.get_lasers_frozen() and (force_update ~= true) then
|
||
minetest.log("info", "[lzr_laser] laser_update skipped (lasers are frozen)")
|
||
return
|
||
end
|
||
local benchmark_time_1 = minetest.get_us_time()
|
||
|
||
local vmanip = minetest.get_voxel_manip(pos1, pos2)
|
||
local vpos1, vpos2 = vmanip:get_emerged_area()
|
||
local varea = VoxelArea:new({MinEdge = vpos1, MaxEdge = vpos2})
|
||
local vdata = vmanip:get_data()
|
||
local vdata_p2 = vmanip:get_param2_data()
|
||
|
||
-- Remember the old state of the map and the state of
|
||
-- detectors, then compare it to the new detector state.
|
||
-- Used for the detector sound effect.
|
||
local detector_states_old = get_detector_states_in_area(pos1, pos2, varea, vdata)
|
||
-- Same for senders
|
||
local sender_states_old = get_sender_states_in_area(pos1, pos2, varea, vdata)
|
||
|
||
if extra_state then
|
||
-- When an active sender was removed, we remember its pos
|
||
-- to play the proper "disable" sound effect.
|
||
if extra_state.removed_active_sender_pos then
|
||
local vi = varea:indexp(extra_state.removed_active_sender_pos)
|
||
sender_states_old[vi] = true
|
||
end
|
||
-- When a node was rotated by the hook, it deferrs the
|
||
-- node update to the VoxelManip
|
||
if extra_state.rotated_pos then
|
||
if varea:containsp(extra_state.rotated_pos) then
|
||
-- This is like minetest.swap_node, but in VManip
|
||
local idx = varea:indexp(extra_state.rotated_pos)
|
||
vdata[idx] = minetest.get_content_id(extra_state.rotated_node.name)
|
||
vdata_p2[idx] = extra_state.rotated_node.param2
|
||
else
|
||
minetest.log("error", "[lzr_laser] extra_state.rotated_pos is out of VManip bounds!")
|
||
end
|
||
end
|
||
end
|
||
|
||
-- << THE MAIN LASER UPDATE HAPPENS HERE >> --
|
||
|
||
-- Step 1: Remove all lasers and deactivate all laser blocks
|
||
-- Step 2: Emit lasers from all emitters, updating
|
||
-- lasers and laser blocks in the process, except
|
||
-- colored detectors.
|
||
-- Step 3: Handle burning cache
|
||
-- Step 4: Activate colored detectors that have been
|
||
-- hit by the right color.
|
||
|
||
-- step 1
|
||
if clear_first ~= false then
|
||
lzr_laser.clear_lasers_in_area(pos1, pos2, varea, vdata)
|
||
end
|
||
|
||
-- step 2
|
||
local emit_state = {
|
||
out_of_bounds = {},
|
||
-- detections table stores which colored detectors have been
|
||
-- hit and by which color. Necessary because we can only
|
||
-- update colored detector state *after* all lasers have
|
||
-- travelled.
|
||
detections = {},
|
||
burning_cache = {},
|
||
destroy_cache = {},
|
||
}
|
||
lzr_laser.emit_lasers_in_area(pos1, pos2, varea, vdata, vdata_p2, emit_state, max_iterations)
|
||
|
||
-- step 3
|
||
-- Trigger node burning for nodes burned by laser
|
||
|
||
local burning_barricades = {}
|
||
local exploding_bombs = {}
|
||
for b=1, #emit_state.burning_cache do
|
||
local vi = emit_state.burning_cache[b]
|
||
local cid = vdata[vi]
|
||
local nodename = minetest.get_name_from_content_id(cid)
|
||
if minetest.get_item_group(nodename, "barricade") == 2 then
|
||
table.insert(burning_barricades, varea:position(vi))
|
||
elseif minetest.get_item_group(nodename, "bomb") == 2 then
|
||
table.insert(exploding_bombs, varea:position(vi))
|
||
end
|
||
end
|
||
if #burning_barricades > 0 then
|
||
local added = false
|
||
local burn_time = BARRICADE_BURN_TIME
|
||
if lzr_gamestate.get_state() == lzr_gamestate.LEVEL_TEST then
|
||
burn_time = burn_time * lzr_globals.LEVEL_TEST_TIME_MULTIPLIER
|
||
end
|
||
local burn_destroy_time = minetest.get_us_time() + burn_time * 1000000
|
||
for d=1, #destroy_events do
|
||
if destroy_events[d].time == burn_destroy_time then
|
||
table.insert_all(destroy_events[d].positions, burning_barricades)
|
||
added = true
|
||
break
|
||
end
|
||
end
|
||
if not added then
|
||
table.insert(destroy_events, {
|
||
time = burn_destroy_time,
|
||
positions = burning_barricades,
|
||
})
|
||
end
|
||
end
|
||
if #exploding_bombs > 0 then
|
||
local added = false
|
||
local burn_time = BOMB_BURN_TIME
|
||
if lzr_gamestate.get_state() == lzr_gamestate.LEVEL_TEST then
|
||
burn_time = burn_time * lzr_globals.LEVEL_TEST_TIME_MULTIPLIER
|
||
end
|
||
local burn_destroy_time = minetest.get_us_time() + burn_time * 1000000
|
||
for d=1, #destroy_events do
|
||
if destroy_events[d].time == burn_destroy_time then
|
||
table.insert_all(destroy_events[d].positions, exploding_bombs)
|
||
added = true
|
||
break
|
||
end
|
||
end
|
||
if not added then
|
||
table.insert(destroy_events, {
|
||
time = burn_destroy_time,
|
||
positions = exploding_bombs,
|
||
})
|
||
end
|
||
end
|
||
|
||
-- step 4
|
||
-- Check and update colored detectors.
|
||
-- (colorless detectors are already dealt with)
|
||
local combined_incoming_colors = {}
|
||
for d=1, #emit_state.detections do
|
||
-- Iterate through all affected colored detectos
|
||
-- and combine the incoming travelling laser colors
|
||
-- When this loop is complete, we have a list of the
|
||
-- *actual* final laser color that hit the detector.
|
||
local detection = emit_state.detections[d]
|
||
local hash = detection.hash
|
||
local colorcode = detection.colorcode
|
||
if combined_incoming_colors[hash] then
|
||
-- colors can be OR'ed
|
||
combined_incoming_colors[hash] = bit.bor(combined_incoming_colors[hash], colorcode)
|
||
else
|
||
combined_incoming_colors[hash] = colorcode
|
||
end
|
||
end
|
||
|
||
|
||
-- Check which colored detectors match the incoming laser
|
||
-- color, then activate those
|
||
for hash, laser_color in pairs(combined_incoming_colors) do
|
||
local pos = minetest.get_position_from_hash(hash)
|
||
local vi = varea:indexp(pos)
|
||
local content_id = vdata[vi]
|
||
local nodename = minetest.get_name_from_content_id(content_id)
|
||
local detector_color = minetest.get_item_group(nodename, "detector_color")
|
||
-- Only an exact color match counts
|
||
if laser_color == detector_color then
|
||
local def = minetest.registered_nodes[nodename]
|
||
local active = def._lzr_active
|
||
if active then
|
||
vdata[vi] = minetest.get_content_id(active)
|
||
end
|
||
end
|
||
end
|
||
|
||
-- << END OF THE MAIN LASER UPDATE >> --
|
||
|
||
-- Required for the detector sound effect.
|
||
local detector_states_new = get_detector_states_in_area(pos1, pos2, varea, vdata)
|
||
|
||
local sender_states_new = get_sender_states_in_area(pos1, pos2, varea, vdata)
|
||
|
||
-- Write laser changes to map
|
||
vmanip:set_data(vdata)
|
||
vmanip:set_param2_data(vdata_p2)
|
||
vmanip:write_to_map()
|
||
|
||
-- Post-map update changes for stuff that the VManip can't do
|
||
|
||
-- <<< OUT-OF-BOUNDS LASERS >>>
|
||
-- Propagate the out-of-bounds lasers.
|
||
-- These are lasers that extend out of the level bounds.
|
||
-- This changes nodes, but doesn't have a gameplay effect, as these
|
||
-- are outside the level.
|
||
-- The start positions and directions of all out-of bounds lasers will be stored
|
||
-- in the out_of_bounds_lasers variable to simplify later removal.
|
||
|
||
-- First step: Mix laser colors of out-of-bounds lasers that start at the same position.
|
||
local real_out_of_bounds = {}
|
||
local real_out_of_bounds_hashes = {}
|
||
for o=1, #emit_state.out_of_bounds do
|
||
local oob = emit_state.out_of_bounds[o]
|
||
local hash = minetest.hash_node_position(oob.pos)
|
||
if real_out_of_bounds[hash] then
|
||
real_out_of_bounds[hash].colorcode = bit.bor(real_out_of_bounds[hash].colorcode, oob.colorcode)
|
||
else
|
||
real_out_of_bounds[hash] = table.copy(oob)
|
||
table.insert(real_out_of_bounds_hashes, hash)
|
||
end
|
||
end
|
||
-- Second step: Place the out-of-bounds laser nodes
|
||
local confirmed_out_of_bounds_lasers = {}
|
||
local barrier_posses = {}
|
||
for o=1, #real_out_of_bounds_hashes do
|
||
local hash = real_out_of_bounds_hashes[o]
|
||
local oob = real_out_of_bounds[hash]
|
||
local oob_lasers, barrier_pos, destroyeds, laser_node = lzr_laser.travel_laser_out_of_bounds(oob.pos, oob.dir, oob.colorcode)
|
||
table.insert_all(out_of_bounds_destroyeds, destroyeds)
|
||
if out_of_bounds_lasers[hash] and out_of_bounds_lasers[hash].colorcode == oob.colorcode then
|
||
-- Out-of-bounds laser with same color already exists, so we don't have to re-add it
|
||
confirmed_out_of_bounds_lasers[hash] = true
|
||
else
|
||
out_of_bounds_lasers[hash] = { pos = oob.pos, dir = oob.dir, colorcode = oob.colorcode}
|
||
confirmed_out_of_bounds_lasers[hash] = true
|
||
-- Unlike the lasers inside the level, out-of-bounds lasers do NOT use the LuaVoxelManip.
|
||
-- They are bulk-set insteaed.
|
||
-- This is because the LuaVoxelManip is only covering the level bounds and a few
|
||
-- nodes beyond, and out-of-bounds lasers are LONG. If we would handle
|
||
-- out-of-bounds lasers in the LuaVoxelManip, we'd have to massively increase
|
||
-- the size of its area which would then increase loading and writing times.
|
||
minetest.bulk_set_node(oob_lasers, laser_node)
|
||
|
||
if barrier_pos then
|
||
-- Penetrate barriers and rain membranes by replacing those
|
||
-- with a special barrier / rain membrane + laser combination node
|
||
local dirs = lzr_laser.vector_and_color_to_dirs(oob.dir, oob.colorcode)
|
||
local dirstring = lzr_laser.dirs_to_dirstring(dirs)
|
||
local bnode = minetest.get_node(barrier_pos)
|
||
if minetest.get_item_group(bnode.name, "rain_membrane") > 0 then
|
||
minetest.set_node(barrier_pos, {name="lzr_laser:rain_membrane_laser_"..dirstring})
|
||
elseif minetest.get_item_group(bnode.name, "barrier") > 0 then
|
||
minetest.set_node(barrier_pos, {name="lzr_laser:barrier_laser_"..dirstring})
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Third step: Remove all no-longer valid out-of-bounds lasers
|
||
lzr_laser.clear_out_of_bounds_lasers(confirmed_out_of_bounds_lasers)
|
||
|
||
-- <<< END OF OUT-OF-BOUNDS LASERS CODE >>>
|
||
|
||
|
||
-- <<< HANDLE TRIGGERS >>> --
|
||
|
||
-- Trigger receivers for all senders that changed state
|
||
for vindex, state_old in pairs(sender_states_old) do
|
||
local state_new = sender_states_new[vindex]
|
||
local pos = varea:position(vindex)
|
||
local activate, deactivate, node_removed = false, false, false
|
||
if (state_new == true and state_old == false) then
|
||
activate = true
|
||
elseif (state_new == false and state_old == true) then
|
||
deactivate = true
|
||
elseif (state_new == nil and state_old == true) then
|
||
deactivate = true
|
||
node_removed = true
|
||
end
|
||
if activate or deactivate then
|
||
if activate then
|
||
minetest.log("info", "[lzr_laser] Sender activates at: "..minetest.pos_to_string(pos))
|
||
else
|
||
minetest.log("info", "[lzr_laser] Sender deactivates at: "..minetest.pos_to_string(pos))
|
||
end
|
||
local trigger_id
|
||
if node_removed then
|
||
trigger_id = extra_state.removed_active_sender_trigger_id
|
||
else
|
||
local tmeta = minetest.get_meta(pos)
|
||
trigger_id = tmeta:get_string("trigger_id")
|
||
end
|
||
if trigger_id == nil or trigger_id == "" then
|
||
-- A state change of a trigger node that has no trigger_id is an error,
|
||
-- unless we're not in an active game.
|
||
local gs = lzr_gamestate.get_state()
|
||
if (gs == lzr_gamestate.LEVEL or gs == lzr_gamestate.LEVEL_TEST) then
|
||
minetest.log("error", "[lzr_laser] Node at "..minetest.pos_to_string(pos).." should have a trigger_id but doesn't have one!")
|
||
end
|
||
break
|
||
end
|
||
local trigger = lzr_triggers.get_trigger(trigger_id)
|
||
if not trigger then
|
||
minetest.log("error", "[lzr_laser] Trigger '"..trigger_id.."' does not exist!")
|
||
break
|
||
end
|
||
local receivers, signal_type
|
||
if not node_removed then
|
||
receivers = lzr_triggers.get_receivers(trigger_id)
|
||
signal_type = trigger.signal_type
|
||
else
|
||
receivers = extra_state.removed_active_sender_send_to
|
||
signal_type = extra_state.removed_active_sender_signal_type
|
||
end
|
||
if not receivers then
|
||
receivers = {}
|
||
end
|
||
for r=1, #receivers do
|
||
local receiver_id = receivers[r]
|
||
local receiver_trigger = lzr_triggers.get_trigger(receiver_id)
|
||
-- Get the trigger location (the node may either be in the map
|
||
-- or player inventory. If it's at neither location, that's an error)
|
||
local rloc = receiver_trigger.location
|
||
local rtype = receiver_trigger.receiver_type
|
||
local rname, rdef, toggle_func_name
|
||
local rpos, rnode
|
||
local ritem, rslot
|
||
|
||
local player = minetest.get_player_by_name("singleplayer")
|
||
if rloc == "player" then
|
||
-- Send signal to node in player inventory
|
||
if player then
|
||
ritem, rslot = lzr_triggers.find_trigger_in_player_inventory(player, receiver_id)
|
||
if ritem then
|
||
rname = ritem:get_name()
|
||
rdef = minetest.registered_nodes[rname]
|
||
toggle_func_name = "_lzr_on_toggle_item"
|
||
end
|
||
end
|
||
elseif type(rloc) == "table" then
|
||
-- Send signal to node in map
|
||
rpos = rloc
|
||
rnode = minetest.get_node(rpos)
|
||
rdef = minetest.registered_nodes[rnode.name]
|
||
rname = rnode.name
|
||
toggle_func_name = "_lzr_on_toggle"
|
||
end
|
||
|
||
if rdef and toggle_func_name and rdef[toggle_func_name] then
|
||
|
||
local receiver_active
|
||
local lb_group = minetest.get_item_group(rname, "lightbox")
|
||
if lb_group > 0 then
|
||
receiver_active = lb_group == 2
|
||
else
|
||
receiver_active = lzr_laser.is_laser_block_active(rname)
|
||
end
|
||
local signal = "NONE"
|
||
-- Send signal state equivalent to our active status
|
||
if signal_type == lzr_triggers.SIGNAL_TYPE_SYNC then
|
||
if activate then
|
||
signal = "ON"
|
||
else
|
||
signal = "OFF"
|
||
end
|
||
-- Send signal state inverse to our active status
|
||
elseif signal_type == lzr_triggers.SIGNAL_TYPE_SYNC_INV then
|
||
if deactivate then
|
||
signal = "ON"
|
||
else
|
||
signal = "OFF"
|
||
end
|
||
-- Send TOGGLE signal on every toggle
|
||
elseif signal_type == lzr_triggers.SIGNAL_TYPE_TOGGLE then
|
||
signal = "TOGGLE"
|
||
-- Send ON signal on every toggle
|
||
elseif signal_type == lzr_triggers.SIGNAL_TYPE_TOGGLE_ON then
|
||
signal = "ON"
|
||
-- Send OFF signal on every toggle
|
||
elseif signal_type == lzr_triggers.SIGNAL_TYPE_TOGGLE_OFF then
|
||
signal = "OFF"
|
||
-- Send ON signal when we activate
|
||
elseif signal_type == lzr_triggers.SIGNAL_TYPE_ACTIVATE_ON then
|
||
if activate then
|
||
signal = "ON"
|
||
end
|
||
-- Send TOGGLE signal when we activate
|
||
elseif signal_type == lzr_triggers.SIGNAL_TYPE_ACTIVATE_TOGGLE then
|
||
if activate then
|
||
signal = "TOGGLE"
|
||
end
|
||
-- Send OFF signal when we activate
|
||
elseif signal_type == lzr_triggers.SIGNAL_TYPE_ACTIVATE_OFF then
|
||
if activate then
|
||
signal = "OFF"
|
||
end
|
||
-- Send ON signal when we deactivate
|
||
elseif signal_type == lzr_triggers.SIGNAL_TYPE_DEACTIVATE_ON then
|
||
if deactivate then
|
||
signal = "ON"
|
||
end
|
||
-- Send TOGGLE signal when we deactivate
|
||
elseif signal_type == lzr_triggers.SIGNAL_TYPE_DEACTIVATE_TOGGLE then
|
||
if deactivate then
|
||
signal = "TOGGLE"
|
||
end
|
||
-- Send OFF signal when we deactivate
|
||
elseif signal_type == lzr_triggers.SIGNAL_TYPE_DEACTIVATE_OFF then
|
||
if deactivate then
|
||
signal = "OFF"
|
||
end
|
||
else
|
||
minetest.log("error", "[lzr_laser] Invalid signal type for sender '"..trigger_id.."': "..tostring(signal_type))
|
||
end
|
||
|
||
-- It is possible a receiver may get triggered many times in the same tick.
|
||
-- This is very likely due to a recursive call when the lasers and triggers are
|
||
-- arranged in such a way that a receiver manages to invoke itself again and
|
||
-- again, forever. This is called "recursive re-activation".
|
||
-- This would cause a stack overflow, so we limit the amount of
|
||
-- times a receiver can be triggered in the same tick. If a certain threshold is
|
||
-- exceeded, the receiver shuts down and refuses to be triggered in this tick
|
||
-- again.
|
||
local check_receiver_overload = function(receiver_id)
|
||
-- Recently touched receivers counts how many times each receivers was
|
||
-- "touched" (i.e. toggled) in this tick.
|
||
if not recently_touched_receivers[receiver_id] then
|
||
recently_touched_receivers[receiver_id] = 1
|
||
return false
|
||
else
|
||
recently_touched_receivers[receiver_id] = recently_touched_receivers[receiver_id] + 1
|
||
end
|
||
if recently_touched_receivers[receiver_id] >= lzr_globals.MAX_RECEIVER_RECALLS then
|
||
return true
|
||
else
|
||
return false
|
||
end
|
||
end
|
||
|
||
local particle_line = function(pos1, pos2, signal)
|
||
local tex
|
||
if signal == "ON" then
|
||
lzr_laser.particle_line(pos1, pos2, "lzr_laser_particle_signal_on.png")
|
||
elseif signal == "OFF" then
|
||
lzr_laser.particle_line(pos1, pos2, "lzr_laser_particle_signal_off.png")
|
||
elseif signal == "TOGGLE" then
|
||
lzr_laser.particle_line(pos1, pos2, "lzr_laser_particle_signal_toggle.png")
|
||
end
|
||
end
|
||
|
||
|
||
-- Send signal from sender to receiver.
|
||
-- This means: The receiver's _on_toggle function is called
|
||
-- when the signal would cause a state change. This also
|
||
-- will spawn some fancy particle lines to make the
|
||
-- signal visible to the player so the trigger relations
|
||
-- can be followed more easily.
|
||
local send_signal = function(signal)
|
||
local toggle_receiver = false
|
||
if signal == "ON" and not receiver_active then
|
||
toggle_receiver = true
|
||
elseif signal == "OFF" and receiver_active then
|
||
toggle_receiver = true
|
||
elseif signal == "TOGGLE" then
|
||
toggle_receiver = true
|
||
end
|
||
|
||
if toggle_receiver and check_receiver_overload(receiver_id) then
|
||
-- If the receiver is overloaded in this, we prevent triggering it on again,
|
||
-- but we allow to shut down one last time.
|
||
if not receiver_active then
|
||
minetest.log("action", "[lzr_laser] Receiver '"..receiver_id.."' shuts down due to recursive re-activation")
|
||
-- Add fancy smoke particles at the node that just shut down
|
||
minetest.add_particlespawner({
|
||
amount = 8,
|
||
time = 0.1,
|
||
minpos = vector.add(rpos, vector.new(-0.4, 0.1, -0.4)),
|
||
maxpos = vector.add(rpos, vector.new(0.4, 0.45, 0.4)),
|
||
minvel = vector.new(-0.1, 0.4, -0.1),
|
||
maxvel = vector.new(0.1, 0.5, 0.1),
|
||
minsize = 4,
|
||
maxsize = 5,
|
||
minexptime = 2,
|
||
maxexptime = 2.85,
|
||
texture = "lzr_laser_overload_smoke.png",
|
||
})
|
||
return
|
||
end
|
||
end
|
||
if toggle_func_name == "_lzr_on_toggle" then
|
||
particle_line(rloc, pos, signal)
|
||
if toggle_receiver then
|
||
rdef._lzr_on_toggle(rloc, rnode)
|
||
end
|
||
minetest.log("info", "[lzr_laser] Sent "..signal.." signal from '"..trigger_id.."' to '"..receiver_id.."'")
|
||
elseif toggle_func_name == "_lzr_on_toggle_item" then
|
||
if player and ritem and rslot then
|
||
local ppos = player:get_pos()
|
||
ppos = vector.offset(ppos, 0, 1, 0)
|
||
particle_line(ppos, pos, signal)
|
||
if toggle_receiver then
|
||
local new_item = rdef._lzr_on_toggle_item(player, ritem, rslot)
|
||
player:get_inventory():set_stack("main", rslot, new_item)
|
||
end
|
||
minetest.log("info", "[lzr_laser] Sent "..signal.." signal from '"..trigger_id.."' to '"..receiver_id.."' (in player inventory)")
|
||
else
|
||
minetest.log("error", "[lzr_laser] Failed to find receiver '"..receiver_id.." in player inventory for "..signal.." from '"..trigger_id.."'!")
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Send the signal
|
||
|
||
-- The simplest type: Just trigger directly
|
||
if rtype == lzr_triggers.RECEIVER_TYPE_ANY then
|
||
send_signal(signal)
|
||
-- Basically a logical AND
|
||
elseif rtype == lzr_triggers.RECEIVER_TYPE_SYNC_AND then
|
||
local my_senders = lzr_triggers.get_senders(receiver_id)
|
||
local all_on = true
|
||
for s=1, #my_senders do
|
||
local ms_id = my_senders[s]
|
||
local ms_trigger = lzr_triggers.get_trigger(ms_id)
|
||
local ms_signal_type = ms_trigger.signal_type
|
||
if ms_signal_type == lzr_triggers.SIGNAL_TYPE_SYNC or ms_signal_type == lzr_triggers.SIGNAL_TYPE_SYNC_INV then
|
||
local ms_is_active = false
|
||
local location = ms_trigger.location
|
||
if type(location) == "table" then
|
||
local ms_node = minetest.get_node(location)
|
||
ms_is_active = lzr_laser.is_laser_block_active(ms_node.name)
|
||
elseif location == "string" then
|
||
local item = lzr_triggers.find_trigger_in_player_inventory(player, ms_id)
|
||
if item then
|
||
ms_is_active = lzr_laser.is_laser_block_active(item:get_name())
|
||
end
|
||
end
|
||
-- The state of senders with this signal type must be inverted for the all_on check
|
||
if ms_signal_type == lzr_triggers.SIGNAL_TYPE_SYNC_INV then
|
||
ms_is_active = not ms_is_active
|
||
end
|
||
if not ms_is_active then
|
||
all_on = false
|
||
break
|
||
end
|
||
end
|
||
end
|
||
if all_on then
|
||
send_signal("ON")
|
||
else
|
||
send_signal("OFF")
|
||
end
|
||
else
|
||
minetest.log("error", "[lzr_laser] Unknown receiver type: "..tostring(rtype))
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
-- <<< END OF TRIGGER HANDLING >>> --
|
||
|
||
-- Play detector sound for all detectors that have changed their state
|
||
for vindex, state_new in pairs(detector_states_new) do
|
||
local state_old = detector_states_old[vindex]
|
||
local pos = varea:position(vindex)
|
||
if state_new == true and state_old == false then
|
||
minetest.sound_play({name="lzr_laser_detector_activate", gain=0.7}, {pos=pos}, true)
|
||
elseif state_new == false and state_old == true then
|
||
minetest.sound_play({name="lzr_laser_detector_deactivate", gain=0.7}, {pos=pos}, true)
|
||
end
|
||
end
|
||
|
||
-- Trigger burning sound for nodes burned by laser
|
||
for b=1, math.min(#emit_state.burning_cache, lzr_globals.MAX_DESTROY_SOUNDS_AT_ONCE) do
|
||
local bpos = varea:position(emit_state.burning_cache[b])
|
||
local bnode = minetest.get_node(bpos)
|
||
if minetest.get_item_group(bnode.name, "barricade") ~= 0 then
|
||
minetest.sound_play({name="lzr_laser_quickburn", gain=1.0}, {pos=bpos}, true)
|
||
elseif minetest.get_item_group(bnode.name, "bomb") ~= 0 then
|
||
minetest.sound_play({name="lzr_laser_bomb_fuse", gain=0.5}, {pos=bpos}, true)
|
||
lzr_laser.spawn_bomb_fuse_particles(bpos, BOMB_BURN_TIME)
|
||
end
|
||
end
|
||
|
||
-- Destroy nodes in destroy_cache (nodes destroyed by laser)
|
||
-- and trigger animation and sound effects
|
||
for d=1, #emit_state.destroy_cache do
|
||
local dpos = emit_state.destroy_cache[d].pos
|
||
local nodename = emit_state.destroy_cache[d].nodename
|
||
local def = minetest.registered_nodes[nodename]
|
||
if def and def.sounds and def.sounds.dug then
|
||
minetest.sound_play(def.sounds.dug, {pos=dpos}, true)
|
||
end
|
||
minetest.add_particlespawner({
|
||
amount = 12,
|
||
time = 0.001,
|
||
minpos = vector.subtract(dpos, vector.new(0.5, 0.5, 0.5)),
|
||
maxpos = vector.add(dpos, vector.new(0.5, 0.5, 0.5)),
|
||
minvel = vector.new(-0.5, -0.2, -0.5),
|
||
maxvel = vector.new(0.5, 0.2, 0.5),
|
||
minacc = vector.new(0, -lzr_globals.GRAVITY, 0),
|
||
maxacc = vector.new(0, -lzr_globals.GRAVITY, 0),
|
||
minsize = 0.8,
|
||
maxsize = 0.8,
|
||
minexptime = 0.5,
|
||
maxexptime = 0.55,
|
||
node = {name=nodename},
|
||
})
|
||
end
|
||
|
||
-- Print benchmark time
|
||
local benchmark_time_2 = minetest.get_us_time()
|
||
local diff = benchmark_time_2 - benchmark_time_1
|
||
minetest.log("info", "[lzr_laser] laser_update took "..diff.." µs.")
|
||
end
|
||
|
||
-- Completely recalculate all lasers in area.
|
||
-- * pos1: Minimum position of area (optional, defaults to level start pos)
|
||
-- * pos2: Maximum position of area (optional, defaults to level end pos)
|
||
-- * extra_state: Info about previous level state
|
||
function lzr_laser.full_laser_update(pos1, pos2, extra_state)
|
||
if not pos1 or not pos2 then
|
||
pos1, pos2 = lzr_world.get_level_bounds()
|
||
end
|
||
laser_update(pos1, pos2, nil, nil, nil, extra_state)
|
||
end
|
||
|
||
-- Freezes the laser simulation. While frozen, lasers will
|
||
-- no longer update automatically on map changes.
|
||
function lzr_laser.freeze_lasers()
|
||
lasers_frozen = true
|
||
end
|
||
|
||
-- Unfreezes the laser simulation. Lasers will
|
||
-- update automatically again.
|
||
function lzr_laser.unfreeze_lasers()
|
||
lasers_frozen = false
|
||
end
|
||
|
||
-- Get current laser frozen state
|
||
function lzr_laser.get_lasers_frozen()
|
||
-- lasers are always frozen in dev mode
|
||
return lasers_frozen or lzr_gamestate.get_state() == lzr_gamestate.DEV
|
||
end
|
||
|
||
-- Several commands for debugging the lasers.
|
||
-- Only available with a hidden setting.
|
||
if minetest.settings:get_bool("lzr_debug", false) then
|
||
|
||
minetest.register_chatcommand("set_freeze_lasers", {
|
||
description = S("Enable or disable frozen lasers. When lasers are frozen, they won’t be updated automatically. Useful for debugging"),
|
||
privs = { debug = true, server = true },
|
||
params = "[on | off]",
|
||
func = function(player, param)
|
||
if param == "" then
|
||
lasers_frozen = not lasers_frozen
|
||
elseif param == "on" then
|
||
lasers_frozen = true
|
||
elseif param == "off" then
|
||
lasers_frozen = false
|
||
else
|
||
return false
|
||
end
|
||
if lasers_frozen then
|
||
return true, S("Lasers are now frozen. Map updates will no longer update the lasers.")
|
||
else
|
||
laser_update(lzr_world.get_level_pos(), vector.add(lzr_world.get_level_pos(), lzr_world.get_level_size()))
|
||
return true, S("Lasers are now unfrozen. Map updates will update the lasers again.")
|
||
end
|
||
end,
|
||
})
|
||
|
||
minetest.register_chatcommand("force_laser_update", {
|
||
description = S("Force a full laser update to occur in the current level boundaries"),
|
||
privs = { debug = true, server = true },
|
||
params = "",
|
||
func = function(player, param)
|
||
local minpos, maxpos = lzr_world.get_level_pos(), vector.add(lzr_world.get_level_pos(), lzr_world.get_level_size())
|
||
laser_update(minpos, maxpos, true)
|
||
return true
|
||
end,
|
||
})
|
||
|
||
minetest.register_chatcommand("emit_lasers", {
|
||
description = S("Emit lasers from all emitters in the current level boundaries"),
|
||
privs = { debug = true, server = true },
|
||
params = S("[<max. iterations>]"),
|
||
func = function(player, param)
|
||
local minpos, maxpos = lzr_world.get_level_pos(), vector.add(lzr_world.get_level_pos(), lzr_world.get_level_size())
|
||
local max_iterations
|
||
local num = tonumber(param)
|
||
if num then
|
||
max_iterations = math.floor(math.max(0, num))
|
||
end
|
||
laser_update(minpos, maxpos, true, false, max_iterations)
|
||
return true
|
||
end,
|
||
})
|
||
|
||
minetest.register_chatcommand("clear_lasers", {
|
||
description = S("Remove all lasers in the current level boundaries and the current out-of-bounds lasers"),
|
||
privs = { debug = true, server = true },
|
||
params = "",
|
||
func = function(player, param)
|
||
local minpos, maxpos = lzr_world.get_level_pos(), vector.add(lzr_world.get_level_pos(), lzr_world.get_level_size())
|
||
local vmanip = minetest.get_voxel_manip(minpos, maxpos)
|
||
local vpos1, vpos2 = vmanip:get_emerged_area()
|
||
local varea = VoxelArea:new({MinEdge = vpos1, MaxEdge = vpos2})
|
||
local vdata = vmanip:get_data()
|
||
lzr_laser.clear_lasers_in_area(minpos, maxpos, varea, vdata)
|
||
|
||
-- Write laser changes to map
|
||
vmanip:set_data(vdata)
|
||
vmanip:write_to_map()
|
||
|
||
lzr_laser.clear_out_of_bounds_lasers()
|
||
|
||
return true
|
||
end,
|
||
})
|
||
|
||
end
|
||
|
||
lzr_laser.reset_destroy_events = function()
|
||
destroy_events = {}
|
||
end
|
||
|
||
local handle_delayed_node_destructions = function()
|
||
-- Remove burning barricades, propagate fire
|
||
-- and explode bombs
|
||
if lzr_laser.get_lasers_frozen() or #destroy_events == 0 then
|
||
return
|
||
end
|
||
local time = minetest.get_us_time()
|
||
local events_to_remove = {}
|
||
local barricades_to_burn = {}
|
||
local bombs_to_explode = {}
|
||
for d=1, #destroy_events do
|
||
local evnt = destroy_events[d]
|
||
if evnt.time <= time then
|
||
for n=1, #evnt.positions do
|
||
local node = minetest.get_node(evnt.positions[n])
|
||
if minetest.get_item_group(node.name, "barricade") == 2 then
|
||
table.insert(barricades_to_burn, evnt.positions[n])
|
||
elseif minetest.get_item_group(node.name, "bomb") == 2 then
|
||
table.insert(bombs_to_explode, evnt.positions[n])
|
||
end
|
||
end
|
||
table.insert(events_to_remove, d)
|
||
end
|
||
end
|
||
if #barricades_to_burn == 0 and #bombs_to_explode == 0 then
|
||
return
|
||
end
|
||
|
||
for t=#events_to_remove, 1 do
|
||
table.remove(destroy_events, events_to_remove[t])
|
||
end
|
||
|
||
local new_positions = lzr_laser.burn_and_destroy({barricades=barricades_to_burn, bombs=bombs_to_explode})
|
||
|
||
local new_position_types = {
|
||
{ name = "barricades", burn_time = BARRICADE_BURN_TIME },
|
||
{ name = "bombs_slow", burn_time = BOMB_BURN_TIME },
|
||
{ name = "bombs_quick", burn_time = BOMB_BURN_TIME_QUICK },
|
||
}
|
||
|
||
for n=1, #new_position_types do
|
||
local ptype = new_position_types[n]
|
||
if #new_positions[ptype.name] > 0 then
|
||
local burn_time = ptype.burn_time
|
||
if lzr_gamestate.get_state() == lzr_gamestate.LEVEL_TEST then
|
||
burn_time = burn_time * lzr_globals.LEVEL_TEST_TIME_MULTIPLIER
|
||
end
|
||
local new_destroy_event = {
|
||
time = time + burn_time * 1000000,
|
||
positions = new_positions[ptype.name],
|
||
}
|
||
table.insert(destroy_events, new_destroy_event)
|
||
end
|
||
end
|
||
|
||
lzr_laser.full_laser_update_if_needed()
|
||
end
|
||
|
||
minetest.register_globalstep(function()
|
||
recently_touched_receivers = {}
|
||
|
||
handle_delayed_node_destructions()
|
||
end)
|
||
|
||
lzr_gamestate.register_on_enter_state(function(state)
|
||
if state ~= lzr_gamestate.LEVEL and state ~= lzr_gamestate.LEVEL_TEST and state ~= lzr_gamestate.LEVEL_COMPLETE then
|
||
lzr_laser.reset_destroy_events()
|
||
end
|
||
end)
|