From e4d4bacc89cf1735780e4cdf3b7ecc67605683ea Mon Sep 17 00:00:00 2001 From: copygirl Date: Wed, 11 Oct 2023 08:36:51 +0200 Subject: [PATCH] Add region type, redo contraption code - Remove utility script - Move vector extensions to dedicated file - Remove vector.convert, add vector.from_hash - Add vector.get to get x,y,z from varargs-ish - contraption:move returns moved nodes --- contraption.lua | 191 +++++++++++++++++++++++------------------- region.lua | 143 +++++++++++++++++++++++++++++++ utility.lua | 46 ---------- vector_extensions.lua | 25 ++++++ 4 files changed, 275 insertions(+), 130 deletions(-) create mode 100644 region.lua delete mode 100644 utility.lua create mode 100644 vector_extensions.lua diff --git a/contraption.lua b/contraption.lua index ed08962..ed2ede4 100755 --- a/contraption.lua +++ b/contraption.lua @@ -4,11 +4,8 @@ local math, pairs, ipairs, setmetatable, table_insert local minetest, vector, nodecore = minetest, vector, nodecore -local utility = include("utility") -local error = utility.error - -local mod_name = minetest.get_current_modname() -local mod_storage = minetest.get_mod_storage() +include("vector_extensions") +local region = include("region") -- This is the object that is returned by this script. local contraption = {} @@ -25,6 +22,10 @@ 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! @@ -46,12 +47,6 @@ local active_contraptions = {} -- 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") @@ -94,6 +89,12 @@ function contraption.on_shutdown() 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 -- ---------------------- @@ -101,13 +102,9 @@ end 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 + 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 end return nil @@ -127,20 +124,17 @@ end local metatable = { __index = contraption } contraption.metatable = metatable -function contraption.create(region) +function contraption.create(r) + r = region.copy(r) + 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 - nodes[offset:hash()] = CONTRAPTION_WOOD - num_nodes = num_nodes + 1 - end - end + for pos in r:iter_node_positions() do + local node = minetest.get_node(pos) + if nodecore.match(node, PLANK) then + local offset = r:to_relative(pos) + nodes[offset:hash()] = CONTRAPTION_WOOD + num_nodes = num_nodes + 1 end end @@ -155,15 +149,15 @@ function contraption.create(region) id_counter = id_counter + 1 local result = setmetatable({ id = id_counter, - region = region, + region = r, 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 = vector.convert(pos_hash) - local abs_pos = region.min + rel_pos + local rel_pos = vector.from_hash(pos_hash) + local abs_pos = r:to_absolute(rel_pos) minetest.set_node(abs_pos, node) end @@ -175,7 +169,7 @@ end function contraption:on_initialize() local above_nodes = {} for orig_pos_hash, node in pairs(self.nodes) do - local orig_pos = vector.convert(orig_pos_hash) + 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. @@ -199,15 +193,12 @@ function contraption.load_from_string(id, str) local result = setmetatable({ id = id, - region = { - min = vector.convert(obj.region.min), - max = vector.convert(obj.region.max) - }, - nodes = obj.nodes, + region = region.copy(obj.region), + nodes = obj.nodes, num_nodes = obj.num_nodes, - motion = obj.motion and vector.convert(obj.motion ) or nil, - partial = obj.partial and vector.convert(obj.partial) or nil, + motion = obj.motion and vector.copy(obj.motion ) or nil, + partial = obj.partial and vector.copy(obj.partial) or nil, }, metatable) result:on_initialize() @@ -233,8 +224,8 @@ function contraption:destroy() active_contraptions[self.id] = nil for pos_hash, node in pairs(self.nodes) do - local rel_pos = vector.convert(pos_hash) - local abs_pos = self.region.min + rel_pos + 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) end @@ -263,35 +254,46 @@ function contraption:update(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 + 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 objects_to_move = {} - local pos1 = self.region.min:offset(-1, -1, -1) - local pos2 = self.region.max:offset( 1, 2, 1) - for _, obj in ipairs(minetest.get_objects_in_area(pos1, pos2)) do - if obj:is_player() then - local adjusted_pos = vector.offset(obj:get_pos(), 0, -0.5, 0) - local rel_pos = adjusted_pos:round() - self.region.min - if self.nodes[rel_pos:hash()] then - table_insert(objects_to_move, obj) - end - end - end - local step = vector.zero() step[which] = math.sign(self.partial[which]) - if self:move(step) then + local moved_nodes = self:move(step) + + if moved_nodes then + -- Reduce partial motion by the amount moved. self.partial = self.partial - step - for _, obj in ipairs(objects_to_move) do obj:set_pos(vector.add(obj:get_pos(), step)) end + + -- 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. @@ -324,6 +326,11 @@ function contraption:update(delta_time) 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`). @@ -336,16 +343,20 @@ function contraption:move(offset) 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.min + rel_pos + 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() } + to_push[rel_pos:hash()] = { + node = node, + meta = minetest.get_meta(abs_pos):to_table() + } end 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.convert(old_rel_pos_hash) - local old_abs_pos = self.region.min + old_rel_pos + local old_rel_pos = vector.from_hash(old_rel_pos_hash) + local old_abs_pos = self.region:to_absolute(old_rel_pos) local new_abs_pos = old_abs_pos + offset local new_rel_pos = old_rel_pos + offset @@ -356,49 +367,59 @@ function contraption:move(offset) local self_overlap = self.nodes[new_rel_pos_hash] if self_overlap then + moved_nodes[old_abs_pos:hash()] = { node = node } if not nodecore.match(node, self_overlap) then - table.insert(to_set, { pos = new_abs_pos, node = node }) + 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() } - table.insert(to_set, { pos = new_abs_pos, node = node }) + 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. - table.insert(to_set, nodecore.underride({ pos = new_abs_pos }, new_node)) + 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.nodes[new_rel_pos_hash] then -- The next node is part of the contraption. -- If it can move, this pushable node can move. - table.insert(to_set, nodecore.underride({ pos = new_abs_pos }, new_node)) + 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. - table.insert(to_set, nodecore.underride({ pos = new_abs_pos }, new_node)) + 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 false + return nil end end elseif nodecore.buildable_to(new_node) then - table.insert(to_set, { pos = new_abs_pos, node = node }) + 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 false + return nil end end end @@ -406,14 +427,14 @@ function contraption:move(offset) -- 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.convert(pos_hash) + 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.min + forward_rel_pos + local forward_abs_pos = self.region:to_absolute(forward_rel_pos) if self.nodes[forward_rel_pos] then -- Do not clear this node by the contraption moving. @@ -424,7 +445,8 @@ function contraption:move(offset) if not nodecore.buildable_to(forward_abs_pos) then goto continue end end - table.insert(to_set, nodecore.underride({ pos = forward_abs_pos }, node)) + 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 @@ -432,8 +454,9 @@ function contraption:move(offset) node = to_push[backward_rel_pos_hash] if not node then break end - local abs_pos = self.region.min + rel_pos - table.insert(to_set, nodecore.underride({ pos = abs_pos }, node)) + 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 @@ -450,15 +473,15 @@ function contraption:move(offset) -- Clear nodes that need to be cleared (to AIR). for pos_hash, node in pairs(to_clear) do - local rel_pos = vector.convert(pos_hash) - local abs_pos = self.region.min + rel_pos + 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 true + return moved_nodes end return contraption diff --git a/region.lua b/region.lua new file mode 100644 index 0000000..290d8d4 --- /dev/null +++ b/region.lua @@ -0,0 +1,143 @@ +local math, setmetatable, vector + = math, setmetatable, vector + +local math_min, math_max, math_floor, math_ceil, math_round + = math.min, math.max, math.floor, math.ceil, math.round + +include("vector_extensions") + +local region = {} +local metatable = { __index = region } +region.metatable = metatable + +local function fast_new(min, max) return setmetatable({ min = min, max = max }, metatable) end + +-- Returns a new region from the specified `min` and `max` position vectors. +function region.new(min, max) return fast_new(vector.copy(min), vector.copy(max)) end +-- Returns a deep copy of this region. Can also be called as `region.copy(r))`. +function region:copy() return region.new(self.min, self.max) end + +-- Returns this region's bounds as an array in the form of +-- `{ min.x, min.y, min.z, max.x, max.y, max.z }`, similar to `collisionbox`. +function region:to_array() return { self.min.x, self.min.y, self.min.z, self.max.x, self.max.y, self.max.z } end +-- Returns a new region from the specified array `a` in the form of +-- `{ min.x, min.y, min.z, max.x, max.y, max.z }`, similar to `collisionbox`. +function region.from_array(a) return fast_new(vector.new(a[1], a[2], a[3]), vector.new(a[4], a[5], a[6])) end +-- Returns a new region from the specified `ObjectRef` using its position and collision box. +function region.from_object(obj) return region.from_array(obj:get_properties().collisionbox):offset_in_place(obj:get_pos()) end + +function region:size() return self.max - self.min end +function region:center() return (self.min + self.max) / 2 end + +-- Offsets this region by the specified vector. +function region:offset_in_place(...) + local x, y, z = vector.get(...) + local min, max = self.min, self.max + min.x = min.x + x + min.y = min.y + y + min.z = min.z + z + max.x = max.x + x + max.y = max.y + y + max.z = max.z + z + return self +end +-- Returns a new region offset by the specified vector. +function region:offset(...) + return self:copy():offset_in_place(...) +end + +-- Expanded this region in all directions by the specified amount. +-- For example, `region:expand_in_place(2)` expands the region by 2 meters in all directions. +function region:expand_in_place(...) + local x, y, z = vector.get(...) + local min, max = self.min, self.max + min.x = min.x - x + min.y = min.y - y + min.z = min.z - z + max.x = max.x + x + max.y = max.y + y + max.z = max.z + z + return self +end +-- Returns a new region expanded in all directions by the specified amount. +-- For example, `region:expand(2)` expands the region by 2 meters in all directions. +function region:expand(...) + return self:copy():expand_in_place(...) +end + +-- Extends this region by the specified vector depending on the signedness of its components. +-- For example, `region:extend_in_place(0, -2, 0)` extends the region downwards by 2 meters. +function region:extend_in_place(...) + local x, y, z = vector.get(...) + local min, max = self.min, self.max + min.x = min.x + math_min(0, x) + min.y = min.y + math_min(0, y) + min.z = min.z + math_min(0, z) + max.x = max.x + math_max(0, x) + max.y = max.y + math_max(0, y) + max.z = max.z + math_max(0, z) + return self +end +-- Returns a new region extended by the specified vector depending on the signedness of its components. +-- For example, `region:extend(0, -2, 0)` extends the region downwards by 2 meters. +function region:extend(...) + return self:copy():extend_in_place(...) +end + +function region:round_in_place() + local min, max = self.min, self.max + min.x = math_round(min.x) + min.y = math_round(min.y) + min.z = math_round(min.z) + max.x = math_round(max.x) + max.y = math_round(max.y) + max.z = math_round(max.z) + return self +end +function region:round() + return self:copy():round_in_place() +end + +-- Returns if the specified `r` overlaps with this region. +function region:overlaps(r) + return r.min.x < self.max.x and r.max.x > self.min.x + and r.min.y < self.max.y and r.max.y > self.min.y + and r.min.z < self.max.z and r.max.z > self.min.z +end + +-- Returns if the specified `pos` is contained within this region. +function region:contains(pos) + return pos.x >= self.min.x and pos.x <= self.max.x + and pos.y >= self.min.y and pos.y <= self.max.y + and pos.z >= self.min.z and pos.z <= self.max.z +end + +function region:to_relative(abs_pos) return vector.subtract(abs_pos, self.min) end +function region:to_absolute(rel_pos) return vector.add(rel_pos, self.min) end + +-- Returns an iterator over the node positions contained in this region. +function region:iter_node_positions() + local min_x = math_ceil(self.min.x) + local min_y = math_ceil(self.min.y) + local min_z = math_ceil(self.min.z) + local max_x = math_floor(self.max.x) + local max_y = math_floor(self.max.y) + local max_z = math_floor(self.max.z) + local x, y, z = min_x, min_y, min_z + return function() + local result = vector.new(x, y, z) + x = x + 1 + if x > max_x then x = min_x; y = y + 1 end + if y > max_y then y = min_y; z = z + 1 end + if z > max_z then return nil end + return result + end +end + +function region.equals(a, b) + return vector.equals(a.min, b.min) + and vector.equals(a.max, b.max) +end +metatable.__eq = region.equals + +return region diff --git a/utility.lua b/utility.lua deleted file mode 100644 index cf4ae51..0000000 --- a/utility.lua +++ /dev/null @@ -1,46 +0,0 @@ -local ipairs, setmetatable, tostring, print - = ipairs, setmetatable, tostring, print - -local minetest, vector - = minetest, vector - -local mod_name = minetest.get_current_modname() - -local utility = {} - --- Prints an error message by concatenating the parameters as strings. -function error(...) - local str = "[" .. mod_name .. "] " - for _, v in ipairs({...}) do str = str + tostring(v) end - print(str) -end -utility.error = utility - -if not vector.convert then - function vector.convert(v) - local t = type(v) - if t == "number" then - -- Assume the number was created by calling `vector.hash`. - return setmetatable(minetest.get_position_from_hash(v), vector.metatable) - elseif t == "string" then - -- Assume the string is either in `(x, y, z)` or `{x=x, y=y, z=z}` format. - local first_chr = v:byte(1) - if first_chr == "(" then return vector.from_string(v) - elseif first_chr == "{" then return setmetatable(minetest.deserialize(v), vector.metatable) - else error("Cannot convert string '" .. v .. "' to vector") end - elseif t == "table" then - -- Assume the table already has `x`, `y` and `z` entries. - -- Returns the same table, making sure its metatable is set. - - return setmetatable(v, vector.metatable) - else error("Cannot convert type '" .. t .. "' to vector") end - end -end - -if not vector.hash then - function vector:hash() - return minetest.hash_node_position(self) - end -end - -return utility diff --git a/vector_extensions.lua b/vector_extensions.lua new file mode 100644 index 0000000..d1aa318 --- /dev/null +++ b/vector_extensions.lua @@ -0,0 +1,25 @@ +local setmetatable, minetest, vector + = setmetatable, minetest, vector + +-- Creates a vector using `get_position_from_hash` from the specified `hash`. +function vector.from_hash(hash) + return setmetatable(minetest.get_position_from_hash(hash), vector.metatable) +end + +-- Returns the integer hash of this vector using `hash_node_position`. +function vector:hash() + return minetest.hash_node_position(self) +end + +-- Returns vector components from the specified arguments. +-- * `(x, y, z)` returns `x, y, z`. +-- * `(number)` returns `n, n, n`. +-- * `(table)` returns `t.x, t.y, t.z`. +-- * `(array)` returns `t[1], t[2], t[3]`. +-- * Unexpected input returns undefined output. +function vector.get(x, y, z) + if z then return x, y, z end + if type(x) == "number" then return x, x, x end + if x.z then return x.x, x.y, x.z end + return x[1], x[2], x[3] +end