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
main
copygirl 7 months ago
parent 5969a2909e
commit e4d4bacc89
  1. 191
      contraption.lua
  2. 143
      region.lua
  3. 46
      utility.lua
  4. 25
      vector_extensions.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

@ -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

@ -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

@ -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
Loading…
Cancel
Save