diff --git a/CONCEPT.md b/CONCEPT.md index 1592020..a20e331 100755 --- a/CONCEPT.md +++ b/CONCEPT.md @@ -3,6 +3,7 @@ ## Progression - Assemble wooden contraption inside a rectangle of frames + - Move items with contraption - Rotate a contraption - Take a contraption to water - Use a shovel to propel a contraption on water diff --git a/contraption.lua b/contraption.lua index ed2ede4..b0f8c15 100755 --- a/contraption.lua +++ b/contraption.lua @@ -102,10 +102,7 @@ end function contraption.find(pos) -- First check if the node at `pos` is a contraption block. for _, c in pairs(active_contraptions) do - if c.region:contains(pos) then - local rel_pos = c.region:to_relative(pos) - if c.nodes[rel_pos:hash()] then return c end - end + if c:get_node_data(pos) then return c end end return nil end @@ -127,13 +124,13 @@ contraption.metatable = metatable function contraption.create(r) r = region.copy(r) - local nodes = {} local num_nodes = 0 - for pos in r:iter_node_positions() do - local node = minetest.get_node(pos) + 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 offset = r:to_relative(pos) - nodes[offset:hash()] = CONTRAPTION_WOOD + 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 @@ -146,37 +143,33 @@ function contraption.create(r) -- 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, - nodes = nodes, + id = id_counter, + region = r, num_nodes = num_nodes, + node_data = node_data, }, metatable) - -- Change nodes from base nodes to their contraption version. - for pos_hash, node in pairs(nodes) do - local rel_pos = vector.from_hash(pos_hash) - local abs_pos = r:to_absolute(rel_pos) - minetest.set_node(abs_pos, node) - end - - result:on_initialize() 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 orig_pos_hash, node in pairs(self.nodes) do - local orig_pos = vector.from_hash(orig_pos_hash) - - -- Calculate the nodes above the contraption that it - -- could potentially pull along with it as it moves. - local above_pos = orig_pos:offset(0, 1, 0) - if not self.nodes[above_pos:hash()] then + 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. - table_insert(above_nodes, above_pos) + above_nodes[pos_hash] = { pos = above_pos } end end self.above_nodes = above_nodes @@ -190,11 +183,21 @@ function contraption.load_from_string(id, str) 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 = region.copy(obj.region), - nodes = obj.nodes, + region = obj.region, + node_data = obj.node_data, num_nodes = obj.num_nodes, motion = obj.motion and vector.copy(obj.motion ) or nil, @@ -207,12 +210,16 @@ function contraption.load_from_string(id, str) 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, - nodes = self.nodes, num_nodes = self.num_nodes, + node_data = node_data, motion = self.motion, partial = self.partial, @@ -223,25 +230,32 @@ end function contraption:destroy() active_contraptions[self.id] = nil - 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 def = minetest.registered_nodes[node.name] - minetest.set_node(abs_pos, def.drop_in_place) + 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 + -- 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) @@ -325,6 +339,252 @@ function contraption:update(delta_time) 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 = + -- 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. @@ -340,23 +600,13 @@ function contraption:move(offset) -- 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.nodes) do to_clear[pos_hash] = AIR end - - for _, rel_pos in ipairs(self.above_nodes) do - local abs_pos = self.region:to_absolute(rel_pos) - local node = minetest.get_node(abs_pos) - if contraption.is_pushable(node) then - to_push[rel_pos:hash()] = { - node = node, - meta = minetest.get_meta(abs_pos):to_table() - } - end - end + 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 old_rel_pos_hash, node in pairs(self.nodes) do - local old_rel_pos = vector.from_hash(old_rel_pos_hash) - local old_abs_pos = self.region:to_absolute(old_rel_pos) + 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 @@ -365,10 +615,10 @@ function contraption:move(offset) -- Node is being moved to this position, no need to clear here. to_clear[new_rel_pos_hash] = nil - local self_overlap = self.nodes[new_rel_pos_hash] + 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) then + if not nodecore.match(node, self_overlap.node) then table_insert(to_set, { pos = new_abs_pos, node = node }) end else @@ -394,7 +644,7 @@ function contraption:move(offset) table_insert(to_set, nodecore.underride({ pos = new_abs_pos }, new_node)) to_push[new_rel_pos_hash] = nil new_node = pushable - elseif self.nodes[new_rel_pos_hash] then + 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 @@ -436,7 +686,7 @@ function contraption:move(offset) local forward_abs_pos = self.region:to_absolute(forward_rel_pos) - if self.nodes[forward_rel_pos] then + if self.node_data[forward_rel_pos] then -- Do not clear this node by the contraption moving. to_clear[forward_rel_pos] = nil else diff --git a/init.lua b/init.lua index 2641dc8..7877b86 100755 --- a/init.lua +++ b/init.lua @@ -4,7 +4,7 @@ local minetest, vector, nodecore, include local search = include("search") local contraption = include("contraption") -local mod_name = minetest.get_current_modname() +local mod_name = minetest.get_current_modname() local i_stick = "nc_tree:stick" local i_staff = "nc_tree:staff" diff --git a/search.lua b/search.lua index dc6b304..11f742a 100755 --- a/search.lua +++ b/search.lua @@ -69,7 +69,7 @@ local function find_rectangle(pos, match) -- direction of the smallest xyz coordinate. if neighbors[1] == -neighbors[2] then -- NOTE: Due to how `get_neighbors_with_name` is written, the first element - -- is guaranteed to contain the vector pointing towards negative. + -- is guaranteed to contain the vector pointing towards negative. pos, neighbors = move_until_corner(pos, match, neighbors[1]) if not pos then return nil end end