|
|
|
local math, pairs, ipairs, setmetatable, table_insert
|
|
|
|
= math, pairs, ipairs, setmetatable, table.insert
|
|
|
|
|
|
|
|
local minetest, vector, nodecore
|
|
|
|
= minetest, vector, nodecore
|
|
|
|
|
|
|
|
include("vector_extensions")
|
|
|
|
local region = include("region")
|
|
|
|
|
|
|
|
-- This is the object that is returned by this script.
|
|
|
|
local contraption = {}
|
|
|
|
|
|
|
|
-- Some constants. Should probably be removed.
|
|
|
|
local AIR = { name = "air" }
|
|
|
|
local PLANK = { name = "nc_woodwork:plank" }
|
|
|
|
local CONTRAPTION_WOOD = { name = "nc_contraptions:contraption_wood" }
|
|
|
|
|
|
|
|
-- Counts up to infinity as contraptions get created.
|
|
|
|
-- Each contraption gets assigned a unique id for its lifetime.
|
|
|
|
local id_counter = 0
|
|
|
|
|
|
|
|
-- Table containing the currently active contraptions keyed by their id.
|
|
|
|
local active_contraptions = {}
|
|
|
|
|
|
|
|
-- Inactive contraptions get stored in mod storage.
|
|
|
|
-- Currently only being used to save contraptions on shutdown.
|
|
|
|
local mod_storage = minetest.get_mod_storage()
|
|
|
|
|
|
|
|
-- TODO: Friction depending on ground.
|
|
|
|
-- TODO: Gravity.
|
|
|
|
-- TODO: Water physics!
|
|
|
|
-- TODO: Rotation.
|
|
|
|
-- TODO: Sound.
|
|
|
|
-- TODO: Discoveries.
|
|
|
|
|
|
|
|
-- TODO: Push items in a pyramid.
|
|
|
|
-- TODO: Damage players colliding with fast-moving contraption.
|
|
|
|
-- TODO: Damage players when squishing them into a wall.
|
|
|
|
-- TODO: Allow glueing separate contraptions together?
|
|
|
|
|
|
|
|
-- TODO: Add region type.
|
|
|
|
-- TODO: Unload and save contraptions when they're in unloaded chunks.
|
|
|
|
-- TODO: Use meta field `infotext` for displaying debug info about a contraption?
|
|
|
|
-- TODO: Use voxel manipulator for moving contraptions.
|
|
|
|
|
|
|
|
--------------------
|
|
|
|
-- EVENT HANDLING --
|
|
|
|
--------------------
|
|
|
|
|
|
|
|
function contraption.on_startup()
|
|
|
|
local version = mod_storage:get_int("version")
|
|
|
|
|
|
|
|
if version == 0 then
|
|
|
|
-- Assume that no mod data has been saved yet.
|
|
|
|
return
|
|
|
|
elseif version ~= 1 then
|
|
|
|
error("Unknown mod storage version " .. version)
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
id_counter = mod_storage:get_int("id_counter")
|
|
|
|
|
|
|
|
local contraptions_str = mod_storage:get("contraptions")
|
|
|
|
local contraptions = minetest.deserialize(contraptions_str)
|
|
|
|
|
|
|
|
for _, id in ipairs(contraptions) do
|
|
|
|
local str = mod_storage:get("contraption_" .. id)
|
|
|
|
contraption.load_from_string(id, str)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function contraption.on_shutdown()
|
|
|
|
-- TODO: Only modify mod storage where contraptions have changed / been destroyed.
|
|
|
|
-- For now we're just going to wipe the storage and re-create it.
|
|
|
|
mod_storage:from_table(nil)
|
|
|
|
|
|
|
|
mod_storage:set_int("version", 1)
|
|
|
|
mod_storage:set_int("id_counter", id_counter)
|
|
|
|
|
|
|
|
local contraptions = {}
|
|
|
|
|
|
|
|
for id, contrap in pairs(active_contraptions) do
|
|
|
|
local str = contrap:save_to_string()
|
|
|
|
mod_storage:set_string("contraption_" .. id, str)
|
|
|
|
|
|
|
|
table_insert(contraptions, id)
|
|
|
|
end
|
|
|
|
|
|
|
|
mod_storage:set_string("contraptions", minetest.serialize(contraptions));
|
|
|
|
end
|
|
|
|
|
|
|
|
function contraption.on_global_step(delta_time)
|
|
|
|
for _, contrap in pairs(active_contraptions) do
|
|
|
|
contrap:update(delta_time)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
----------------------
|
|
|
|
-- PUBLIC FUNCTIONS --
|
|
|
|
----------------------
|
|
|
|
|
|
|
|
function contraption.find(pos)
|
|
|
|
-- First check if the node at `pos` is a contraption block.
|
|
|
|
for _, c in pairs(active_contraptions) do
|
|
|
|
if c:get_node_data(pos) then return c end
|
|
|
|
end
|
|
|
|
return nil
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Returns if the specified node is pushable by a contraption, like items.
|
|
|
|
function contraption.is_pushable(node)
|
|
|
|
local def = minetest.registered_nodes[node.name]
|
|
|
|
if def and def.groups and def.groups.is_stack_only then return true end
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
|
|
|
-------------------------------------
|
|
|
|
-- CONTRAPTION TYPE IMPLEMENTATION --
|
|
|
|
-------------------------------------
|
|
|
|
|
|
|
|
local metatable = { __index = contraption }
|
|
|
|
contraption.metatable = metatable
|
|
|
|
|
|
|
|
function contraption.create(r)
|
|
|
|
r = region.copy(r)
|
|
|
|
|
|
|
|
local num_nodes = 0
|
|
|
|
local node_data = {}
|
|
|
|
for abs_pos in r:iter_node_positions() do
|
|
|
|
local node = minetest.get_node(abs_pos)
|
|
|
|
if nodecore.match(node, PLANK) then
|
|
|
|
local rel_pos = r:to_relative(abs_pos)
|
|
|
|
node_data[rel_pos:hash()] = { rel_pos = rel_pos, node = CONTRAPTION_WOOD }
|
|
|
|
num_nodes = num_nodes + 1
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Less than 4 nodes don't make a contraption.
|
|
|
|
if num_nodes < 4 then return nil end
|
|
|
|
|
|
|
|
-- TODO: Shrink region to the minimum required cuboid.
|
|
|
|
-- In the above code we could just use pos as table key.
|
|
|
|
|
|
|
|
-- TODO: Flood fill algorithm to make sure all nodes are connected.
|
|
|
|
|
|
|
|
-- Change nodes from base nodes to their contraption version.
|
|
|
|
for _, data in pairs(node_data) do
|
|
|
|
minetest.set_node(data.pos, data.node)
|
|
|
|
end
|
|
|
|
|
|
|
|
id_counter = id_counter + 1
|
|
|
|
local result = setmetatable({
|
|
|
|
id = id_counter,
|
|
|
|
region = r,
|
|
|
|
num_nodes = num_nodes,
|
|
|
|
node_data = node_data,
|
|
|
|
}, metatable)
|
|
|
|
|
|
|
|
active_contraptions[result.id] = result
|
|
|
|
result:on_initialize()
|
|
|
|
return result
|
|
|
|
end
|
|
|
|
|
|
|
|
function contraption:on_initialize()
|
|
|
|
-- Calculate the nodes above the contraption that it
|
|
|
|
-- could potentially pull along with it as it moves.
|
|
|
|
local above_nodes = {}
|
|
|
|
for pos_hash, data in pairs(self.node_data) do
|
|
|
|
local above_pos = data.pos:offset(0, 1, 0)
|
|
|
|
if not self:get_node_data(above_pos) then
|
|
|
|
-- Non-contraption block above this one. Move items here along.
|
|
|
|
above_nodes[pos_hash] = { pos = above_pos }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
self.above_nodes = above_nodes
|
|
|
|
end
|
|
|
|
|
|
|
|
function contraption.load_from_string(id, str)
|
|
|
|
local obj = minetest.deserialize(str)
|
|
|
|
|
|
|
|
if obj.version ~= 1 then
|
|
|
|
error("Unknown contraption version " .. obj.version)
|
|
|
|
return nil
|
|
|
|
end
|
|
|
|
|
|
|
|
-- TODO: Remove this temporary backward-compatibility.
|
|
|
|
obj.node_data = obj.node_data or obj.nodes
|
|
|
|
|
|
|
|
-- Recover "unnecessary" data.
|
|
|
|
obj.region = region.copy(obj.region)
|
|
|
|
for pos_hash, node in pairs(obj.node_data) do
|
|
|
|
local rel_pos = vector.from_hash(pos_hash)
|
|
|
|
obj.node_data[pos_hash] = { rel_pos = rel_pos, node = node }
|
|
|
|
end
|
|
|
|
|
|
|
|
local result = setmetatable({
|
|
|
|
id = id,
|
|
|
|
|
|
|
|
region = obj.region,
|
|
|
|
node_data = obj.node_data,
|
|
|
|
num_nodes = obj.num_nodes,
|
|
|
|
|
|
|
|
motion = obj.motion and vector.copy(obj.motion ) or nil,
|
|
|
|
partial = obj.partial and vector.copy(obj.partial) or nil,
|
|
|
|
}, metatable)
|
|
|
|
|
|
|
|
result:on_initialize()
|
|
|
|
active_contraptions[id] = result
|
|
|
|
return obj
|
|
|
|
end
|
|
|
|
|
|
|
|
function contraption:save_to_string()
|
|
|
|
local node_data = {} -- Clear out unnecessary data.
|
|
|
|
for pos_hash, data in pairs(self.node_data) do
|
|
|
|
node_data[pos_hash] = data.node end
|
|
|
|
|
|
|
|
local to_save = {
|
|
|
|
version = 1,
|
|
|
|
|
|
|
|
region = self.region,
|
|
|
|
num_nodes = self.num_nodes,
|
|
|
|
node_data = node_data,
|
|
|
|
|
|
|
|
motion = self.motion,
|
|
|
|
partial = self.partial,
|
|
|
|
}
|
|
|
|
return minetest.serialize(to_save)
|
|
|
|
end
|
|
|
|
|
|
|
|
function contraption:destroy()
|
|
|
|
active_contraptions[self.id] = nil
|
|
|
|
|
|
|
|
for _, data in pairs(self.node_data) do
|
|
|
|
local def = minetest.registered_nodes[data.node.name]
|
|
|
|
minetest.set_node(data.pos, def.drop_in_place)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function contraption:get_node_data(pos, is_relative)
|
|
|
|
if not is_relative then pos = self.region:to_relative(pos) end
|
|
|
|
return self.node_data[pos:hash()]
|
|
|
|
end
|
|
|
|
|
|
|
|
function contraption:push(pusher, dir)
|
|
|
|
local can_move = not not self:simulate_move(dir)
|
|
|
|
local has_moved = not not self:move(dir)
|
|
|
|
minetest.chat_send_all("can_move=" .. tostring(can_move) .. " :: has_moved=" .. tostring(has_moved))
|
|
|
|
|
|
|
|
-- See if the same player has already pushed this contraption recently.
|
|
|
|
-- If so, do not apply the full amount of motion from the push.
|
|
|
|
-- local pusher_name = pusher.get_player_name and pusher:get_player_name() or ""
|
|
|
|
-- self.recent_pusher = self.recent_pusher or {}
|
|
|
|
-- local delay = self.recent_pusher[pusher_name]
|
|
|
|
-- if delay then dir = dir * (1 - math.min(1, delay)) end
|
|
|
|
-- self.recent_pusher[pusher_name] = 1.0 -- seconds
|
|
|
|
|
|
|
|
-- self.motion = self.motion or vector.zero()
|
|
|
|
-- self.motion = self.motion + dir
|
|
|
|
end
|
|
|
|
|
|
|
|
function contraption:update(delta_time)
|
|
|
|
if not self.motion then return end
|
|
|
|
|
|
|
|
-- Partial motion that accumulates over time,
|
|
|
|
-- since we can only move in increments of 1.
|
|
|
|
self.partial = self.partial or vector.zero()
|
|
|
|
self.partial = self.partial + self.motion * delta_time
|
|
|
|
|
|
|
|
-- If partial motion has increased to more than 1 on
|
|
|
|
-- one axis, find out which axis has the largest motion ..
|
|
|
|
local which = 0
|
|
|
|
do
|
|
|
|
local max = 0
|
|
|
|
for i = 1, 3 do
|
|
|
|
local abs = math.abs(self.partial[i])
|
|
|
|
if abs >= 1 and abs > max then
|
|
|
|
max = abs
|
|
|
|
which = i
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- .. and move it 1 step into that direction.
|
|
|
|
if which ~= 0 then
|
|
|
|
local step = vector.zero()
|
|
|
|
step[which] = math.sign(self.partial[which])
|
|
|
|
local moved_nodes = self:move(step)
|
|
|
|
|
|
|
|
if moved_nodes then
|
|
|
|
-- Reduce partial motion by the amount moved.
|
|
|
|
self.partial = self.partial - step
|
|
|
|
|
|
|
|
-- Push players around.
|
|
|
|
local r = self.region:expand(2):extend_in_place(step)
|
|
|
|
for _, player in ipairs(minetest.get_objects_in_area(r.min, r.max)) do
|
|
|
|
if player:is_player() then
|
|
|
|
local extends = region.from_object(player)
|
|
|
|
extends:expand_in_place(0.5)
|
|
|
|
extends:extend_in_place(0, -0.5, 0)
|
|
|
|
extends:extend_in_place(-step)
|
|
|
|
|
|
|
|
for pos in extends:iter_node_positions() do
|
|
|
|
if moved_nodes[pos:hash()] then
|
|
|
|
-- TODO: Add velocity instead of teleporting.
|
|
|
|
player:set_pos(vector.add(player:get_pos(), step))
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
else
|
|
|
|
-- Reset motion into the direction that we bumped into.
|
|
|
|
-- Allows for "sliding" along walls instead of stopping outright.
|
|
|
|
self.motion[which] = 0
|
|
|
|
self.partial[which] = 0
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Apply friction.
|
|
|
|
local FRICTION = 0.25
|
|
|
|
self.motion = self.motion * (1 - FRICTION * delta_time)
|
|
|
|
|
|
|
|
-- If motion is small enough, unset it for performance.
|
|
|
|
if self.motion:length() < 0.1 then
|
|
|
|
self.motion = nil
|
|
|
|
self.partial = nil
|
|
|
|
end
|
|
|
|
|
|
|
|
if self.recent_pusher then
|
|
|
|
local has_pushers = false
|
|
|
|
for pusher, delay in pairs(self.recent_pusher) do
|
|
|
|
if delay > 0 then
|
|
|
|
self.recent_pusher[pusher] = math.max(0, delay - delta_time)
|
|
|
|
has_pushers = true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
-- If there are no active pushers, delete the `recent_pushers` table.
|
|
|
|
if not has_pushers then self.recent_pusher = nil end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function contraption:simulate_move(offset)
|
|
|
|
local success = true
|
|
|
|
local affected = {
|
|
|
|
-- [pos_hash] = {
|
|
|
|
-- type = "fixed" -- Part of the contraption, must move.
|
|
|
|
-- or "dragged" -- Dragged on top, may or may not move.
|
|
|
|
-- or "pushed" -- Pushed in front, must move.
|
|
|
|
-- or "replace" -- Replaced by another node.
|
|
|
|
-- or "blocks" -- Blocks movement of the contraption.
|
|
|
|
-- rel_pos = vector
|
|
|
|
-- abs_pos = vector
|
|
|
|
-- node = node
|
|
|
|
-- meta = true or nil
|
|
|
|
-- target = <affected>
|
|
|
|
-- success = bool or nil when not calculated yet
|
|
|
|
-- }
|
|
|
|
}
|
|
|
|
|
|
|
|
for pos_hash, data in pairs(self.node_data) do
|
|
|
|
local abs_pos = self.region:to_absolute(data.rel_pos);
|
|
|
|
affected[pos_hash] = {
|
|
|
|
type = "fixed",
|
|
|
|
rel_pos = data.rel_pos,
|
|
|
|
abs_pos = abs_pos,
|
|
|
|
node = data.node,
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
for pos_hash, data in pairs(self.above_nodes) do
|
|
|
|
local abs_pos = self.region:to_absolute(data.rel_pos);
|
|
|
|
local node = minetest.get_node(data.abs_pos)
|
|
|
|
if contraption.is_pushable(node) then
|
|
|
|
affected[pos_hash] = {
|
|
|
|
type = "dragged",
|
|
|
|
rel_pos = data.rel_pos,
|
|
|
|
abs_pos = abs_pos,
|
|
|
|
node = node,
|
|
|
|
meta = true,
|
|
|
|
}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Collect additional relevant nodes.
|
|
|
|
local newly_affected = {}
|
|
|
|
local function check_target(data)
|
|
|
|
local offset_rel_pos = data.rel_pos + offset
|
|
|
|
local offset_abs_pos = data.abs_pos + offset
|
|
|
|
local offset_pos_hash = offset_rel_pos:hash()
|
|
|
|
|
|
|
|
local target = affected[offset_pos_hash]
|
|
|
|
if target then
|
|
|
|
-- If the node ahead is "fixed" (part of the contraption),
|
|
|
|
-- then it has to be able to move for the whole to move.
|
|
|
|
-- We can consider this node to be able to move forward.
|
|
|
|
data.target = target
|
|
|
|
if target.type == "fixed" then
|
|
|
|
data.success = true
|
|
|
|
end
|
|
|
|
else
|
|
|
|
target = { rel_pos = offset_rel_pos, abs_pos = offset_abs_pos,
|
|
|
|
node = minetest.get_node(offset_abs_pos) }
|
|
|
|
newly_affected[offset_pos_hash] = target
|
|
|
|
data.target = target
|
|
|
|
|
|
|
|
if data.type == "fixed" and contraption.is_pushable(target.node) then
|
|
|
|
target.type = "pushed"
|
|
|
|
target.meta = true
|
|
|
|
check_target(target)
|
|
|
|
|
|
|
|
-- Target node can be replaced, so do so.
|
|
|
|
elseif nodecore.buildable_to(target.node) then
|
|
|
|
target.type = "replace"
|
|
|
|
data.success = true
|
|
|
|
|
|
|
|
-- Target node blocks movement.
|
|
|
|
else
|
|
|
|
target.type = "blocks"
|
|
|
|
data.success = false
|
|
|
|
success = false
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
for _, data in pairs(affected) do check_target(data) end
|
|
|
|
for pos_hash, data in newly_affected do affected[pos_hash] = data
|
|
|
|
|
|
|
|
|
|
|
|
for _, data in pairs(affected) do
|
|
|
|
local offset_rel_pos = data.rel_pos + offset
|
|
|
|
local offset_abs_pos = data.abs.pos + offset
|
|
|
|
local offset_pos_hash = offset_rel_pos:hash()
|
|
|
|
|
|
|
|
data.target = affected[offset_pos_hash]
|
|
|
|
|
|
|
|
if data.target then
|
|
|
|
if data.target.type == "fixed" then
|
|
|
|
data.success = true
|
|
|
|
else
|
|
|
|
-- TODO: Need to check of the node in front can move.
|
|
|
|
-- Probably gonna add a set of nodes to check.
|
|
|
|
end
|
|
|
|
else
|
|
|
|
data.target = { rel_pos = offset_rel_pos, abs_pos = offset_abs_pos,
|
|
|
|
node = minetest.get_node(offset_abs_pos) }
|
|
|
|
newly_affected[offset_pos_hash] = data.target
|
|
|
|
|
|
|
|
if data.type == "fixed" then
|
|
|
|
if contraption.is_pushable(data.target.node) then
|
|
|
|
data.target.type = "pushed"
|
|
|
|
|
|
|
|
offset_rel_pos = offset_rel_pos + offset
|
|
|
|
offset_abs_pos = offset_abs_pos + offset
|
|
|
|
offset_pos_hash = offset_rel_pos:hash()
|
|
|
|
|
|
|
|
data.target.target = affected[offset_pos_hash]
|
|
|
|
|
|
|
|
if data.target.target then
|
|
|
|
if data.target.type == "fixed" then
|
|
|
|
data.success = true
|
|
|
|
end
|
|
|
|
else
|
|
|
|
data.target.target = { rel_pos = offset_rel_pos, abs_pos = offset_abs_pos,
|
|
|
|
node = minetest.get_node(offset_abs_pos) }
|
|
|
|
newly_affected[offset_pos_hash] = data.target.target
|
|
|
|
|
|
|
|
if nodecore.buildable_to(data.target.node) then
|
|
|
|
data.target.type = "replace"
|
|
|
|
data.success = true
|
|
|
|
else
|
|
|
|
data.target.type = "blocks"
|
|
|
|
data.success = false
|
|
|
|
success = false
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if data.success == nil then
|
|
|
|
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local function try_move(pos_hash, rel_pos, data, can_push)
|
|
|
|
local offset_rel_pos = rel_pos + offset
|
|
|
|
local offset_abs_pos = data.pos + offset
|
|
|
|
local offset_pos_hash = offset_rel_pos:hash()
|
|
|
|
local offset_data
|
|
|
|
|
|
|
|
local result_entry = result[offset_pos_hash]
|
|
|
|
if result_entry then
|
|
|
|
-- `result` already has an entry at the offset position.
|
|
|
|
-- This means that node has already been checked and can move.
|
|
|
|
offset_data = result_entry.from
|
|
|
|
|
|
|
|
else
|
|
|
|
offset_data = { pos = offset_abs_pos }
|
|
|
|
|
|
|
|
local contraption_node = self.nodes[offset_pos_hash]
|
|
|
|
if contraption_node then
|
|
|
|
-- There's a contraption at the offset position.
|
|
|
|
-- If the whole can move, this node will be able to as well.
|
|
|
|
offset_data.node = contraption_node
|
|
|
|
|
|
|
|
else
|
|
|
|
local pushable_data = to_push[offset_pos_hash]
|
|
|
|
if pushable_data then
|
|
|
|
-- Node is pushable and was going to be pushed anyway.
|
|
|
|
|
|
|
|
-- TODO: Get rid of this nonsense.
|
|
|
|
-- Node has already been processed by another pushable node.
|
|
|
|
if pushable_data.can_move ~= nil then return pushable_data.can_move end
|
|
|
|
|
|
|
|
offset_data = pushable_data
|
|
|
|
local success = try_move(offset_pos_hash, offset_rel_pos, offset_data, false)
|
|
|
|
if to_push_iterating then pushable_data.can_move = success
|
|
|
|
else to_push[offset_pos_hash] = nil end
|
|
|
|
if not success then return false end
|
|
|
|
|
|
|
|
else
|
|
|
|
offset_data.node = minetest.get_node(offset_abs_pos)
|
|
|
|
|
|
|
|
if can_push and contraption.is_pushable(offset_data.node) then
|
|
|
|
-- There's a pushable node in the way, so try to push it.
|
|
|
|
-- Only one up to one node can be pushed this way.
|
|
|
|
offset_data.meta = true -- Grab meta data for the pushed node later.
|
|
|
|
if not try_move(offset_pos_hash, offset_rel_pos, offset_data, false) then return false end
|
|
|
|
|
|
|
|
elseif nodecore.buildable_to(offset_data.node) then
|
|
|
|
-- Offset node can be replaced by this one.
|
|
|
|
|
|
|
|
else
|
|
|
|
-- Bumped into something.
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Successfully moved, add to result.
|
|
|
|
result[pos_hash] = { from = data, to = offset_data }
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Attempt to move nodes that are part of the contraption.
|
|
|
|
-- If any of them can't be moved, the contraption can't move.
|
|
|
|
for pos_hash, node in pairs(self.nodes) do
|
|
|
|
local rel_pos = vector.from_hash(pos_hash)
|
|
|
|
local abs_pos = self.region:to_absolute(rel_pos)
|
|
|
|
local data = { pos = abs_pos, node = node }
|
|
|
|
if not try_move(pos_hash, rel_pos, data, true) then return nil end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- `to_push` contains "lose" nodes that are not directly
|
|
|
|
-- moved by the contraption, and may or may not be pushed.
|
|
|
|
to_push_iterating = true
|
|
|
|
for pos_hash, pushable_data in pairs(to_push) do
|
|
|
|
if result[pos_hash] then
|
|
|
|
local rel_pos = vector.from_hash(pos_hash)
|
|
|
|
local data = { pos = pushable_data.abs_pos, node = pushable_data.node, meta = true }
|
|
|
|
pushable_data.can_move = try_move(pos_hash, rel_pos, data, false)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Fill in the meta for each entry where requested.
|
|
|
|
-- It's not needed until we know the contraption can move.
|
|
|
|
for _, data in pairs(result) do
|
|
|
|
if data.meta == true then
|
|
|
|
data.meta = minetest.get_meta(data.pos):to_table()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
return result
|
|
|
|
end
|
|
|
|
|
|
|
|
function contraption.update_world(moved_nodes)
|
|
|
|
local to_set = {} -- Nodes to set because they have changed.
|
|
|
|
|
|
|
|
for pos_hash, data in pairs(moved_nodes) do
|
|
|
|
to_set[pos_hash] = { node = AIR }
|
|
|
|
end
|
|
|
|
|
|
|
|
for _, data in pairs(moved_nodes) do
|
|
|
|
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function contraption:move(offset)
|
|
|
|
-- Hash table of absolute positions before moved to nodes that have been moved by the
|
|
|
|
-- contraption, either because they're part of it, or have been pushed as a result.
|
|
|
|
local moved_nodes = {}
|
|
|
|
-- TODO: Just have a function that returns the nodes-to-moved (if possible) and a separate one to actually do the moving?
|
|
|
|
|
|
|
|
-- `to_clear` and `to_push` use a relative position hash
|
|
|
|
-- as their key, since entries will need to be removed.
|
|
|
|
local to_clear = {} -- Table of nodes to be cleared (to `AIR`).
|
|
|
|
local to_push = {} -- Table of nodes that want to be pushed.
|
|
|
|
-- `to_set` is an append-only array with `{ pos, node, [meta] }` entries.
|
|
|
|
local to_set = {} -- Array of nodes that will be modified.
|
|
|
|
|
|
|
|
-- Assume that every node this contraption occupies needs to be cleared.
|
|
|
|
-- We'll remove entries from `to_clear` when we know a node is moved there.
|
|
|
|
for pos_hash in pairs(self.node_data) do to_clear[pos_hash] = AIR end
|
|
|
|
|
|
|
|
-- TODO: Redo this loop to work similar to the `to_push` loop below.
|
|
|
|
for _, data in pairs(self.node_data) do
|
|
|
|
local node = data.node
|
|
|
|
local old_abs_pos = data.pos
|
|
|
|
local old_rel_pos = self.region:to_relative(old_abs_pos)
|
|
|
|
|
|
|
|
local new_abs_pos = old_abs_pos + offset
|
|
|
|
local new_rel_pos = old_rel_pos + offset
|
|
|
|
local new_rel_pos_hash = new_rel_pos:hash()
|
|
|
|
|
|
|
|
-- Node is being moved to this position, no need to clear here.
|
|
|
|
to_clear[new_rel_pos_hash] = nil
|
|
|
|
|
|
|
|
local self_overlap = self.node_data[new_rel_pos_hash]
|
|
|
|
if self_overlap then
|
|
|
|
moved_nodes[old_abs_pos:hash()] = { node = node }
|
|
|
|
if not nodecore.match(node, self_overlap.node) then
|
|
|
|
table_insert(to_set, { pos = new_abs_pos, node = node })
|
|
|
|
end
|
|
|
|
else
|
|
|
|
local new_node = minetest.get_node(new_abs_pos)
|
|
|
|
-- TODO: Could also just check `to_push` first instead of checking the node.
|
|
|
|
if contraption.is_pushable(new_node) then
|
|
|
|
new_node = { node = new_node, meta = minetest.get_meta(new_abs_pos):to_table() }
|
|
|
|
moved_nodes[old_abs_pos:hash()] = new_node
|
|
|
|
table_insert(to_set, { pos = new_abs_pos, node = node })
|
|
|
|
|
|
|
|
while true do
|
|
|
|
old_abs_pos = new_abs_pos
|
|
|
|
old_rel_pos = new_rel_pos
|
|
|
|
new_abs_pos = new_abs_pos + offset
|
|
|
|
new_rel_pos = new_rel_pos + offset
|
|
|
|
new_rel_pos_hash = new_rel_pos:hash()
|
|
|
|
|
|
|
|
local pushable = to_push[new_rel_pos_hash]
|
|
|
|
if pushable then
|
|
|
|
-- The next node is a pushable node that was going to
|
|
|
|
-- get pushed by the contraption anyway, so continue.
|
|
|
|
moved_nodes[old_abs_pos:hash()] = new_node
|
|
|
|
table_insert(to_set, nodecore.underride({ pos = new_abs_pos }, new_node))
|
|
|
|
to_push[new_rel_pos_hash] = nil
|
|
|
|
new_node = pushable
|
|
|
|
elseif self.node_data[new_rel_pos_hash] then
|
|
|
|
-- The next node is part of the contraption.
|
|
|
|
-- If it can move, this pushable node can move.
|
|
|
|
moved_nodes[old_abs_pos:hash()] = new_node
|
|
|
|
table_insert(to_set, nodecore.underride({ pos = new_abs_pos }, new_node))
|
|
|
|
-- Do not clear this node by the contraption moving.
|
|
|
|
to_clear[new_rel_pos_hash] = nil
|
|
|
|
break
|
|
|
|
elseif nodecore.buildable_to(new_abs_pos) then
|
|
|
|
-- The next node is replaceable, so we can replace it.
|
|
|
|
moved_nodes[old_abs_pos:hash()] = new_node
|
|
|
|
table_insert(to_set, nodecore.underride({ pos = new_abs_pos }, new_node))
|
|
|
|
break
|
|
|
|
else
|
|
|
|
-- There's a node in the way, abort!
|
|
|
|
-- NOTE: Can't push a pushable node with another
|
|
|
|
-- one if already pushed by the contraption.
|
|
|
|
return nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
elseif nodecore.buildable_to(new_node) then
|
|
|
|
moved_nodes[old_abs_pos:hash()] = { node = node }
|
|
|
|
table_insert(to_set, { pos = new_abs_pos, node = node })
|
|
|
|
else
|
|
|
|
-- We bumped into a wall or something.
|
|
|
|
return nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- The `to_push` table contains "lose" nodes that are not directly
|
|
|
|
-- pushed by the contraption, and may or may not be pushed.
|
|
|
|
for pos_hash, node in pairs(to_push) do
|
|
|
|
local rel_pos = vector.from_hash(pos_hash)
|
|
|
|
local forward_rel_pos = rel_pos + offset
|
|
|
|
|
|
|
|
-- If there's a node to push ahead of this one, skip.
|
|
|
|
-- The node ahead will take care of pulling the ones behind along with it.
|
|
|
|
if to_push[forward_rel_pos:hash()] then goto continue end
|
|
|
|
|
|
|
|
local forward_abs_pos = self.region:to_absolute(forward_rel_pos)
|
|
|
|
|
|
|
|
if self.node_data[forward_rel_pos] then
|
|
|
|
-- Do not clear this node by the contraption moving.
|
|
|
|
to_clear[forward_rel_pos] = nil
|
|
|
|
else
|
|
|
|
-- If the node(s) can't be pushed, then just don't, but continue
|
|
|
|
-- moving the rest of the contraption and other pushable nodes.
|
|
|
|
if not nodecore.buildable_to(forward_abs_pos) then goto continue end
|
|
|
|
end
|
|
|
|
|
|
|
|
moved_nodes[self.region:to_absolute(rel_pos):hash()] = node
|
|
|
|
table_insert(to_set, nodecore.underride({ pos = forward_abs_pos }, node))
|
|
|
|
|
|
|
|
while true do
|
|
|
|
local backward_rel_pos = rel_pos - offset
|
|
|
|
local backward_rel_pos_hash = backward_rel_pos:hash()
|
|
|
|
node = to_push[backward_rel_pos_hash]
|
|
|
|
if not node then break end
|
|
|
|
|
|
|
|
local abs_pos = self.region:to_absolute(rel_pos)
|
|
|
|
moved_nodes[self.region:to_absolute(backward_rel_pos):hash()] = node
|
|
|
|
table_insert(to_set, nodecore.underride({ pos = abs_pos }, node))
|
|
|
|
rel_pos = backward_rel_pos
|
|
|
|
pos_hash = backward_rel_pos_hash
|
|
|
|
end
|
|
|
|
|
|
|
|
to_clear[pos_hash] = AIR
|
|
|
|
::continue::
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Set nodes that need to be changed.
|
|
|
|
for _, e in ipairs(to_set) do
|
|
|
|
minetest.set_node(e.pos, e.node)
|
|
|
|
if e.meta then minetest.get_meta(e.pos):from_table(e.meta) end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Clear nodes that need to be cleared (to AIR).
|
|
|
|
for pos_hash, node in pairs(to_clear) do
|
|
|
|
local rel_pos = vector.from_hash(pos_hash)
|
|
|
|
local abs_pos = self.region:to_absolute(rel_pos)
|
|
|
|
minetest.set_node(abs_pos, node)
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Move the contraption in the world.
|
|
|
|
self.region.min = self.region.min + offset
|
|
|
|
self.region.max = self.region.max + offset
|
|
|
|
return moved_nodes
|
|
|
|
end
|
|
|
|
|
|
|
|
return contraption
|