local math, pairs, ipairs, setmetatable, table_insert, print = math, pairs, ipairs, setmetatable, table.insert, print local minetest, vector, nodecore = minetest, vector, nodecore local mod_name = minetest.get_current_modname() local mod_storage = minetest.get_mod_storage() -- 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 = {} -- TODO: Push along items. -- TODO: Push along players. -- TODO: Friction depending on ground. -- TODO: Gravity. -- TODO: Water physics! -- TODO: Rotation. -- TODO: Unload and save contraptions when they're in unloaded chunks. -- TODO: Use meta field `infotext` for displaying debug info about a contraption? local function error(...) local str = "[" .. mod_name .. "] " for _, v in ipairs({...}) do str = str + tostring(v) end print(str) end -------------------- -- EVENT HANDLING -- -------------------- function contraption.on_global_step(delta_time) for _, contrap in pairs(active_contraptions) do contrap:update(delta_time) end end 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 ---------------------- -- 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 pos.x >= c.region.min.x and pos.x <= c.region.max.x and pos.y >= c.region.min.y and pos.y <= c.region.max.y and pos.z >= c.region.min.z and pos.z <= c.region.max.z then local rel_pos = pos - c.region.min local rel_pos_hash = minetest.hash_node_position(rel_pos) if c.nodes[rel_pos_hash] then return c end end end return nil end ------------------------------------- -- CONTRAPTION TYPE IMPLEMENTATION -- ------------------------------------- local metatable = { __index = contraption } contraption.metatable = metatable function contraption.create(region) local nodes = {} local num_nodes = 0 for x = region.min.x, region.max.x do for y = region.min.y, region.max.y do for z = region.min.z, region.max.z do local pos = vector.new(x, y, z) local node = minetest.get_node(pos) if nodecore.match(node, PLANK) then local offset = pos - region.min local pos_hash = minetest.hash_node_position(offset) nodes[pos_hash] = CONTRAPTION_WOOD num_nodes = num_nodes + 1 end end 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. id_counter = id_counter + 1 local result = setmetatable({ id = id_counter, region = region, nodes = nodes, num_nodes = num_nodes, }, metatable) -- Change nodes from base nodes to their contraption version. for pos_hash, node in pairs(nodes) do local rel_pos = minetest.get_position_from_hash(pos_hash) local abs_pos = vector.add(region.min, rel_pos) minetest.set_node(abs_pos, node) end active_contraptions[result.id] = result return result 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 local result = setmetatable({ id = id, region = obj.region, nodes = obj.nodes, 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) active_contraptions[id] = result return obj end function contraption:save_to_string() local to_save = { version = 1, region = self.region, nodes = self.nodes, num_nodes = self.num_nodes, motion = self.motion, partial = self.partial, } return minetest.serialize(to_save) end function contraption:destroy() active_contraptions[self.id] = nil for pos_hash, node in pairs(self.nodes) do local rel_pos = minetest.get_position_from_hash(pos_hash) local abs_pos = vector.add(self.region.min, rel_pos) local def = minetest.registered_nodes[node.name] minetest.set_node(abs_pos, def.drop_in_place) end end function contraption:push(pusher, dir) -- 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 = vector.add(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 max = 0 local which = 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 -- .. and move it 1 step into that direction. if which ~= 0 then local step = vector.zero() step[which] = math.sign(self.partial[which]) if self:move(step) then self.partial = self.partial - step 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:move(offset) local to_set = {} -- Nodes that need to be modified. local to_clear = {} -- Nodes to be cleared (to AIR). -- 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 old_rel_pos_hash, node in pairs(self.nodes) do local old_rel_pos = minetest.get_position_from_hash(old_rel_pos_hash) local old_abs_pos = vector.add(self.region.min, old_rel_pos) local new_abs_pos = vector.add(old_abs_pos, offset) local new_rel_pos = vector.add(old_rel_pos, offset) local new_rel_pos_hash = minetest.hash_node_position(new_rel_pos) -- 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] if self_overlap then if not nodecore.match(node, self_overlap) then to_set[new_rel_pos_hash] = node end else local new_node = minetest.get_node(new_abs_pos) if nodecore.buildable_to(new_node) then to_set[new_rel_pos_hash] = node else -- We bumped into a wall or something. return false end end end -- Set nodes that need to be changed. for pos_hash, node in pairs(to_set) do local rel_pos = minetest.get_position_from_hash(pos_hash) local abs_pos = vector.add(self.region.min, rel_pos) minetest.set_node(abs_pos, node) end -- Clear nodes that need to be cleared (to AIR). for pos_hash, node in pairs(to_clear) do local rel_pos = minetest.get_position_from_hash(pos_hash) local abs_pos = vector.add(self.region.min, 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 true end return contraption