poschangelib/init.lua

323 lines
11 KiB
Lua

poschangelib = {
player_pos_listeners = {},
walk_listeners = {},
}
dofile(minetest.get_modpath(minetest.get_current_modname()) .. '/register.lua')
--[[
-- File table of contents
-- 1. Settings and utilities
-- 2. Player position listener functions
-- 3. On walk listener functions
-- 4. Tools for main loop
-- 5. Main loop
--]]
function poschangelib.setting_check_interval()
return tonumber(minetest.settings:get('poschangelib_check_interval')) or 0.3
end
function poschangelib.setting_teleport_range()
return tonumber(minetest.settings:get('poschangelib_teleport_range')) or 10
end
--- Table of already called listeners in main loop to prevent triggering them
-- more than once per loop (player) if they are registered for more than one event
-- (for example triggered on walk on multiple groups)
local triggered_listeners = {}
local function set_listener_triggered(listener_name, pos)
if not triggered_listeners[listener_name] then
triggered_listeners[listener_name] = {}
end
table.insert(triggered_listeners[listener_name], pos)
end
--- Internal utility to create an empty table on first registration.
-- @param mothertable The main table that will hold other tables.
-- @param item Key in the main table that should hold a table.
-- @return The table in mothertable.item, created if nil.
local function get_subtable_or_create(mothertable, item)
if mothertable.item == nil then
mothertable.item = {}
end
return mothertable.item
end
--- Check if a listener can be triggered
local function is_callable(listener_name, pos)
-- Check if not aleady called
if triggered_listeners[listener_name] then
for _, trigg_pos in ipairs(triggered_listeners[listener_name]) do
if vector.equals(trigg_pos, pos) then
return false
end
end
end
-- Other checks will come here when required
return true
end
local function copy_trigger_meta(meta)
local new_meta = {}
for i, key in pairs({'interpolated', 'teleported', 'source',
'source_level', 'redo', 'covered'}) do
new_meta[key] = meta[key]
end
if meta.player_pos then
new_meta.player_pos = vector.new(meta.player_pos.x, meta.player_pos.y, meta.player_pos.z)
end
return new_meta
end
--- Trigger registered callbacks if not already triggered.
-- Reset triggered_listeners to be able to recall the callback.
local function trigger_player_position_listeners(player, old_pos, pos, trigger_meta)
for name, callback in pairs(poschangelib.player_pos_listeners) do
if is_callable(name, pos) then
callback(player, old_pos, pos, trigger_meta)
set_listener_triggered(name, pos)
end
end
end
--- Trigger a walk listener by it's name.
-- Never called directly, use trigger_player_walk_listener_by_* functions
local function trigger_player_walk_listeners(trigger_name, player, pos, node, node_def, trigger_meta)
for listener_name, callback in pairs(poschangelib.walk_listeners[trigger_name]) do
if is_callable(listener_name, pos) then
callback(player, pos, node, node_def, trigger_meta)
set_listener_triggered(listener_name, pos)
end
end
end
--- Check if a walk listener can be triggered by node name and trigger it.
-- Trigger meta is copied and extended before being passed to the listeners.
local function trigger_player_walk_listeners_by_node_name(player, pos, node, node_def, trigger_meta)
if poschangelib.walk_listeners[node.name] then
local new_meta = copy_trigger_meta(trigger_meta)
new_meta.source = node.name
trigger_player_walk_listeners(node.name, player, pos, node, node_def, new_meta)
end
end
--- Check if a walk listener can be triggered by node groups and trigger it.
-- Trigger meta is copied and extended before being passed to the listeners.
local function trigger_player_walk_listeners_by_node_group(player, pos, node, node_def, trigger_meta)
local groups_below = node_def.groups
if groups_below then
for group, level in pairs(groups_below) do
local group_name = 'group:' .. group
if level > 0 and poschangelib.walk_listeners[group_name] then
local new_meta = copy_trigger_meta(trigger_meta)
new_meta.source = group
new_meta.source_level = level
trigger_player_walk_listeners(group_name, player, pos, node, node_def, new_meta)
end
end
end
end
local function trigger_on_walk(player, pos, node, node_def, trigger_meta)
if node_def._on_walk then
node_def._on_walk(pos, node, player, copy_trigger_meta(trigger_meta))
elseif node_def.on_walk then
node_def.on_walk(pos, node, player, copy_trigger_meta(trigger_meta))
end
end
--[[
-- Tools for main loop
--]]
--- Table of last rounded registered position of each players.
local player_last_pos = {}
local function remove_last_pos_on_leave(player)
player_last_pos[player:get_player_name()] = nil
end
minetest.register_on_leaveplayer(remove_last_pos_on_leave)
--- Erratically get a path from start_pos and end_pos. This won't be 100%
-- accurate for many reasons.
-- - We don't know if a node is passable or not.
-- - There may be multiple options to get from one point to an other with the
-- same length
-- - The player may not even walk straight
-- This function is recursive, start will move toward end.
-- @param start_pos Full coortinate of starting point (recursive)
-- @param end_pos The goal
-- @param path Empty at start, will contains all points between start and end
-- at the last call, then return up all the way to the first call.
function poschangelib.get_path(start_pos, end_pos, path)
-- Try to get closer to end_pos by moving one block in the axis that
-- is the further from end. If at the same distance for more than one
-- axis, pick randomly between them.
if path == nil then path = {} end
table.insert(path, start_pos)
local distance = vector.subtract(end_pos, start_pos)
-- Check for teleportation
local teleport_range = poschangelib.setting_teleport_range()
local dX = math.abs(distance.x)
local dY = math.abs(distance.y)
local dZ = math.abs(distance.z)
if (dX + dY + dZ <= 1) or
(teleport_range > 0 and dX + dY + dZ > teleport_range) then
-- Next step will reach end_pos
-- or teleported
table.insert(path, end_pos)
return path
end
local d = {} -- List of candidates axis for next move
if dX >= dY and dX >= dZ then table.insert(d, 'x') end
if dY >= dX and dY >= dZ then table.insert(d, 'y') end
if dZ >= dX and dZ >= dY then table.insert(d, 'z') end
local axis = d[math.random(1, table.getn(d))]
local next_pos = nil
if axis == 'x' then
if distance.x > 0 then
next_pos = vector.add(start_pos, vector.new(1,0,0))
else
next_pos = vector.add(start_pos, vector.new(-1,0,0))
end
elseif axis == 'y' then
if distance.y > 0 then
next_pos = vector.add(start_pos, vector.new(0,1,0))
else
next_pos = vector.add(start_pos, vector.new(0,-1,0))
end
elseif axis == 'z' then
if distance.z > 0 then
next_pos = vector.add(start_pos, vector.new(0,0,1))
else
next_pos = vector.add(start_pos, vector.new(0,0,-1))
end
end
if axis == nil then
minetest.log('error', 'poschangelib interpolator is lost')
return path
end
return poschangelib.get_path(next_pos, end_pos, path)
end
--- Check if position has changed for the player.
-- @param player The player object.
-- @returns List of positions from last known to current
-- (with guessed interpolation) if the position has changed, nil otherwise.
local function get_updated_positions(player)
local pos = vector.round(player:get_pos())
local old_pos = player_last_pos[player:get_player_name()]
local ret = nil
if old_pos == nil then
-- Position of the player was set
ret = {pos}
elseif pos then
-- Check for position change
if not vector.equals(old_pos, pos) then
ret = poschangelib.get_path(old_pos, pos)
end
end
player_last_pos[player:get_player_name()] = pos
return ret
end
--- Check and call on_walk triggers if required.
local function check_on_walk_triggers(player, old_pos, pos, trigger_meta)
if trigger_meta == nil then trigger_meta = {} end
-- Get the node at current player position to check if in mid-air
-- or on a half-filled node.
local pos_below = pos
local node_below = minetest.get_node(pos)
local node_def = minetest.registered_nodes[node_below.name]
if not node_def then return end -- Unknown node, don't crash
-- When the feet are not directly on the node below, the player may be
-- in-air or standing on a non-filled walkable block.
-- Pass this information to the listener in case they want a fine
-- collision checking.
if not trigger_meta.interpolated then
trigger_meta.player_pos = pos
end
if not node_def.walkable then
-- Player not standing in a non-filled node
-- Check node below, if walkable consider the player is walking
-- on it (not 100% accurate)
local node_above = node_below
local node_above_def = node_def
pos_below = vector.new(pos.x, pos.y - 1, pos.z)
node_below = minetest.get_node(pos_below)
node_def = minetest.registered_nodes[node_below.name]
if not node_def then return end
if not node_def.walkable then return end -- truely not walking
-- We have checked the node above, see if it covers the one below
-- and trigger walk for that node.
if node_above.name ~= 'air' then
trigger_player_walk_listeners_by_node_name(player, pos, node_above, node_above_def, trigger_meta)
trigger_player_walk_listeners_by_node_group(player, pos, node_above, node_above_def, trigger_meta)
trigger_on_walk(player, pos, node_above, node_above_def, trigger_meta)
-- Set covered for the node below
trigger_meta.covered = true
end
else
-- Player standing inside a walkable node (like a slab or snow).
-- But when coming from above (hooked to a nearby filled node)
-- it may have already been triggered (but maybe ignored because
-- it had a fine collision check).
if old_pos.y - 1 == pos.y then
-- Already triggered from above, pass this information
trigger_meta.redo = true
end
end
-- Triggers
trigger_player_walk_listeners_by_node_name(player, pos_below, node_below, node_def, trigger_meta)
trigger_player_walk_listeners_by_node_group(player, pos_below, node_below, node_def, trigger_meta)
trigger_on_walk(player, pos_below, node_below, node_def, trigger_meta)
end
dofile(minetest.get_modpath(minetest.get_current_modname()) .. '/stomping.lua')
--[[
-- Main loop
--]]
local function loop()
local teleport_range = poschangelib.setting_teleport_range()
-- Player checks
for _, player in ipairs(minetest.get_connected_players()) do
local poss = get_updated_positions(player)
if poss then
local pos_count = table.getn(poss)
if pos_count == 1 then
-- Moved from nil to a given position
trigger_player_position_listeners(player, nil, poss[0])
elseif pos_count == 2 then
-- Non-interpolated movement
local teleported = false
local trigger_meta = {}
if teleport_range > 0 and vector.distance(poss[1], poss[2]) >= teleport_range then
trigger_meta.teleported = true
end
trigger_player_position_listeners(player, poss[1], poss[2], trigger_meta)
check_on_walk_triggers(player, poss[1], poss[2], trigger_meta)
else
-- Interpolated movement
local poss_end_couple = table.getn(poss) - 1
for i = 1, poss_end_couple do
local trigger_meta = {}
if i > 1 and i <= poss_end_couple then
trigger_meta.interpolated = true
end
trigger_player_position_listeners(player, poss[i], poss[i+1], trigger_meta)
check_on_walk_triggers(player, poss[i], poss[i+1], trigger_meta)
end
end
-- Reset the triggered listener to allow the next player to trigger them
triggered_listeners = {}
end
end
minetest.after(poschangelib.setting_check_interval(), loop)
end
minetest.after(poschangelib.setting_check_interval(), loop)