commit 2575c1012146df3f5813d819637e1bfd4edb2e2f Author: OldCoder Date: Sun Sep 4 22:03:05 2022 -0700 Imported from trollstream "ContentDB" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1e1d0c0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,14 @@ +License of source code + +GNU Lesser General Public License, version 2.1 +Copyright (C) 2017 Karamel +With knowledge from various Minetest developers, modders and documenters + +This program is free software; you can redistribute it and/or modify it under the terms +of the GNU Lesser General Public License as published by the Free Software Foundation; +either version 2.1 of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU Lesser General Public License for more details: +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..09272f3 --- /dev/null +++ b/README.md @@ -0,0 +1,275 @@ +Minetest mod library: poschangelib +================================== + +* version 0.5 +* Licence LGPLv2 or, at your discretion, any later version. + +This lib adds utilities to watch player movements and trigger things when they are +spotted moving. + +It does nothing by itself but aim to ease event based upon players or item +moving. + +All positions are rounded to node position (integer coordinates). + +Summary + - General warning + - Watch players' movements + - Watch players walking on particular nodes + - Add _on_walk on nodes + - Set stomping on nodes + - Add footprints + - Configuration/Performances tweaking + - Debugging + + +General warning +--------------- + +This mod may be resources consuming. The mods relying upon this lib should use +small functions not to decrease the server performances too much. + +The more functions are provided, the more the server can lag (but probably a little +less than running every of them without the lib). + + + +Watch player's movements +------------------------ + +Use poschangelib.add_player_pos_listener(name, my_callback) + +Name is the identifier the listener, to use in remove_player_pos_listener. You should +follow the naming convention like for node names. See http://dev.minetest.net/Intro + +The my_callback is a function that takes 4 arguments: the player, last known position, +new position and some metadata. + +On first call (once a player joins) the last known position will be nil. If your +listener does something in that case, it will be called shortly after the player +reconnects. It may so be triggered twice from the same position, before leaving and +after joining. + +Be aware that the new position may not always be a neighbour of the old one. +When on teleporting, programatic moves with setpos or moving fast it may be far away. + +Quick code sample: + +local function my_callback(player, old_pos, new_pos, meta) + if old_pos == nil then + minetest.chat_send_player(player:get_player_name(), 'Welcome to the world!') + else + minetest.chat_send_player(player:get_player_name(), + "You are now at x:" .. new_pos.x .. ", y:" .. new_pos.y .. + "z:" .. new_pos.z) + end +end +poschangelib.add_player_pos_listener("sample:pos_listener", my_callback) + + + +Watch player walking on particular nodes, the rough way +------------------------------------------------------- + +Use poschangelib.add_player_walk_listener(name, my_callback, nodenames) + +The name is used in the same way as for player position listeners. It aims at reducing +the number of time the stepped node is fetched to share it accross all listeners. + +The callback is a function that takes 4 arguments: the player, the position, the node +stepped on and that node description. +See http://dev.minetest.net/minetest.register_node for node description. + +You can register the listener for a list of node name or groups, in the same way you +do it to register an ABM. See http://dev.minetest.net/register_abm + +For example: +local function flop(player, pos, node, desc) + minetest.chat_send_player(player:get_player_name(), 'Flop flop') +end +poschangelib.add_player_walk_listener('sample:flop', flop, {'default:dirt_with_grass'}) + +local function toptop(player, pos, node, desc) + minetest.chat_send_player(player:get_player_name(), 'Top top top') +end +poschangelib.add_player_walk_listener('sample:top', toptop, {'group:choppy'}) + + + +Watch player walking on particular nodes, the fine way +------------------------------------------------------ + +When dealing with non-filled blocks like slab and snow, the trigger may give some +false positives and be triggered twice for the same movement. This is because you can +hook to a nearby full block and stand above snow without touching it, which messes +with the walk detection of regular blocks (which checks for walkable nodes). + +Moreover it can't be enough. With the example of slabs, lower slabs can be triggered +by hanging to a nearby full block and should not be triggered that way, but higher +slabs must be considered like full blocks, because the player is walking on the above +node. + +If you don't require an accurate checking, just ignore the call when trigger_meta.redo +is true like in the example below: + +local function toptop(player, pos, node, desc, trigger_meta) + if trigger_meta.redo then return end + ... do your regular stuff +end + +If you want to make fine position checking, you can use the 5th argument which holds +the trigger metadata. See "More on metadata" below. + + + +Add _on_walk_over to nodes +------------------------- + +This behaviour is ported from the walkover mod only for compatibility. +https://forum.minetest.net/viewtopic.php?f=9&t=15991 + +A new node property can be added in node definitions: + _on_walk_over = + +This function takes the position, the node and the player as argument. + +For compatibility with walkover, you can use on_walk_over (without the underscore +prefix) but it is discouraged as stated in the forum post. This support may be dropped +at any time when most mods have updated the name. + +_on_walk is affected by the same issue about non-filled nodes. You can use the 4th +argument to check the trigger metadata to adjust your callback. + + + +More on metadata +---------------- + +The metadata are a table that can contain the following elements: + +interpolated +Is true when the position was assumed and not observed. Most of the time because the +player moved too fast to check all nodes in real time. + +teleported +Is true when the player was moving too fast. The interpolation is then not computed. + +player_pos +Is set for walk listeners, it contains the player's position. Not set when +interpolated. + +source +Contains the name of the node or group that triggered the walk listener. +This is one of thoses passed on registration. + +source_level +Contains the level of the group when source is a node group. + +redo +Is true when it was detected that the listener was previously called on that position. + +covered +Is true when a non-walkable non-air node is present above this node (like grass). +Covered is accurate only with full nodes. For half-filled node, you'll have to check +by hand. + + + +Set stomping on nodes +--------------------- + +Stomping is a dedicated subset of walk listeners that allows to replace a node by an +other when a player walks on it. + +It is required to be able to declare multiple outputs without messing with one +another. And just for ease of use. + +Stomping are registered with poschangelib.register_stomp. It takes 3 parameters: + +poschangelib.register_stomp: +- source_node_name: the name of the node that can be stomped. It can be a table + with multiple node names to declare the same stomping behaviour to multiple + nodes at once. +- stomp_node_name: the name of the replacement node, or a function. +- stomp_desc: stomping parameters. + +The stomp description is a table that can contains the following set of keys: + +stomp_desc: +- chance: inverted chance that the stomp occurs (default 1) +- duration: time in second after which the stomp reverts. + When not set, the stomp is forever. If set it will override duration_min and + duration_max. +- duration_min: same as duration but to add some randomness for each node. +- duration_max: same as duration but to add some randomness for each node. +- priority: the priority rank. The lower, the more important it is (default 100) +- name: name that is used as walk listener name. + Default is __to__ and is rather indigest but probably unique. + It has no default when using a function in stomp_node_name and must be set. +- source_node: set it if you want the stomp to revert to an other node than the + original. + +When multiple stompings are registered for the same node, only the first +triggered is applied. This is when priority comes into play. When a player walks +on a node that can be stomped, a roll is made for each stomp in order of +priority (the lowest priority first). If the roll succeeds, the node is replaced +and the next stomps are not run. + +When using a function instead of a stomp node name, this function is a regular +player walk listener. It must return a node or nil (i.e. {name = , +param = etc}). If it returns nil, the stomp is not done and the priority +check is not stopped (see just below). When using only a node name, all other +node values are kept. + + + + +Add footprints +-------------- + +Use poschangelib.register_footprint to quickly register footprinted nodes and +the stomping associated to it. The function takes 2 parameters: + +register_footprint: +- node_name: the name of the node to extend, or a table to register multiple + footprints with the same stomp_desc +- stomp_desc: see above. + +The stomp description can have dedicated keys and values: + +- footprint_texture: set it to use an other texture than the one embedded. + +A new node will be registered with most of it's description copied from the +original node. It's top texture will have the footprint layer on it and the +stomping behaviour will be automatically created. + +poschangelib.register_footprint returns the footprinted node name(s). If you +pass nested tables in node_name, the same nesting is returned. + + + +Configuration/Performances tweaking +----------------------------------- + +The lib checks for position at a given interval. Default is every 0.3 seconds. + +This can be changed by setting poschangelib.check_interval in minetest.conf +or in advanced settings. + +Setting a lower value will make the lib more accurate but will be more demanding +on resources (down to 0.05 which is a every server tick). + +If the server is lagging, try increasing the interval. If the server can afford +more precise checks you can decrease the value. + + + +Debugging +--------- + +With the server privileges, you can list available stompings for the node you +are currently on. Use /stomp. + +If there is only one stomping available, it is triggered. If there are multiple +stomps, it prints the list of stomping names. Use /stomp X to trigger the Xth +stomp. diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..3af6445 --- /dev/null +++ b/init.lua @@ -0,0 +1,322 @@ +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) diff --git a/mod.conf b/mod.conf new file mode 100644 index 0000000..075355c --- /dev/null +++ b/mod.conf @@ -0,0 +1 @@ +description = Players' position observing library diff --git a/register.lua b/register.lua new file mode 100644 index 0000000..ee194f6 --- /dev/null +++ b/register.lua @@ -0,0 +1,64 @@ +--[[ +-- Player position listeners +--]] + +--- Register a callback that will be called everytime a player moves. +-- @param name Unique name of the callback. Used to remove. +-- @param callback Callback function. Take , , arguments. +-- The first call will have set to nil. +function poschangelib.add_player_pos_listener(name, callback) + if poschangelib.player_pos_listeners[name] then + minetest.log('error', 'Player pos listener ' .. name .. ' is already registered') + return + end + poschangelib.player_pos_listeners[name] = callback +end + +--- Remove a registered callback. It won't be called anymore. +function poschangelib.remove_player_pos_listener(name) + if poschangelib.player_pos_listeners[name] then + poschangelib.player_pos_listeners[name] = nil + end +end + +--[[ +-- Walk listeners +--]] + +--- Register a callback that will be called everytime a player moves on a block. +-- @param callback Callback function. Takes , , , +-- as arguments. +-- Node is the node below the player's position. +-- @param nodenames List of node names or group (with 'group:X') to observe. +-- The callback will be triggered only if the block has the same name or +-- has one of these groups. +function poschangelib.add_player_walk_listener(name, callback, nodenames) + for _, nodename in ipairs(nodenames) do + if not poschangelib.walk_listeners[nodename] then + poschangelib.walk_listeners[nodename] = {} + end + if poschangelib.walk_listeners[nodename][name] then + minetest.log('error', 'Walk listener ' .. name .. ' is already registered') + end + poschangelib.walk_listeners[nodename][name] = callback + end +end + +function poschangelib.remove_player_walk_listener(name, nodenames) + local counts = {} + for _, nodename in ipairs(nodenames) do + if not counts[nodename] then counts[nodename] = 0 end + counts[nodename] = counts[nodename] + 1 + if poschangelib.walk_listeners[nodename] and poschangelib.walk_listeners[nodename][name] then + poschangelib.walk_listeners[nodename][name] = nil + counts[nodename] = counts[nodename] - 1 + end + end + -- If no listener left for the group, remove the group + -- to be able to skip node check if there are none left + for _, nodename in pairs(counts) do + if counts[nodename] == 0 then + poschangelib.walk_listeners[nodename] = nil + end + end +end diff --git a/settingtypes.txt b/settingtypes.txt new file mode 100644 index 0000000..a119265 --- /dev/null +++ b/settingtypes.txt @@ -0,0 +1,8 @@ +# Interval in seconds between position checking +# The lesser it is, the more accurate it is but also the more resources demanding. +poschangelib_check_interval (Check interval) float 0.3 0.05 + +# Distance between two checks that is considered to be a teleportation and won't +# compute interpolated positions between last known position and current position. +# Set to 0 to disable checking. +poschangelib_teleport_range (Teleport range) int 10 0 diff --git a/stomping.lua b/stomping.lua new file mode 100644 index 0000000..4dc8688 --- /dev/null +++ b/stomping.lua @@ -0,0 +1,286 @@ +--[[ +This file contains the stomping layer. +It is dedicated to transform a node to an other when walked on. +--]] + +local function table_copy(table) + local orig_type = type(table) + local copy = {} + if orig_type ~= 'table' then return table end + for orig_key, orig_value in next, table, nil do + copy[orig_key] = table_copy(orig_value) + end + return copy +end + + +--- Store all registered stomped nodes indexed by source node name +-- For every node name there can be a list of stomping descriptions, ordered +-- by priority in ascending order. +local stomps = {} + +--- Get default stomp name to use with listeners. +local function get_stomp_name(source_node_name, stomp_node_name, mod_name) + if not mod_name then + mod_name = minetest.get_current_modname() + end + return mod_name .. ':' .. source_node_name .. '__to__' .. stomp_node_name +end + +function poschangelib.get_footprint_node_name(source_node_name, mod_name) + if not mod_name then + -- current_modname is the caller mod, not always poschangelib + mod_name = minetest.get_current_modname() + end + local node_mod_name = string.sub(source_node_name, 1, string.find(source_node_name, ':')) + if node_mod_name == mod_name then + return source_node_name .. '_with_footprint' + else + return mod_name .. ':' .. string.gsub(source_node_name, ':', '__') .. '_with_footprint' + end +end + +--- poschangelib walk callback +local function walk_listener(player, pos, node, desc, trigger_meta) + poschangelib.chance_stomp(player, pos, node, desc, trigger_meta) +end + +--- Random roll and do the stomp if it succeeds. +function poschangelib.chance_stomp(player, pos, node, node_desc, trigger_meta) + local stomp_desc = stomps[node.name] + if not stomp_desc then + minetest.log('warning', 'No stomping data found for node ' .. node.name) + return + end + for i, s_desc in ipairs(stomp_desc) do + if (math.random() * s_desc.chance) < 1.0 then + poschangelib.do_stomp(player, pos, node, node_desc, s_desc, trigger_meta) + return + end + end +end + +--- Actually do the stomp: replace the stomped node. +-- @param player The player the triggered the stomp. +-- @param pos Position of the stomped node. +-- @param node Node being stomped. +-- @param node_desc Description of the node being stomped. +-- @param stomp_desc Optional stomp description. If not provided it looks for it. +-- @param trigger_meta Optional trigger meta, passed by walk listeners. +function poschangelib.do_stomp(player, pos, node, node_desc, stomp_desc, trigger_meta) + if not stomp_desc then + stomp_desc = stomps[node.name] + if stomp_desc then stomp_desc = stomp_desc[1] end + end + if not stomp_desc then + minetest.log('warning', 'No stomping data found for node ' .. node.name) + return + end + if not trigger_meta then trigger_meta = {} end + if type(stomp_desc.dest_node_name) == 'function' then + local dest_node = stomp_desc.dest_node_name(player, pos, node, trigger_meta) + if not dest_node then return end + if not dest_node.name then + minetest.log('error', 'Stomping: function did not set node name for ' .. node.name) + return + end + minetest.set_node(pos, dest_node) + else + local new_node = minetest.get_node(pos) + new_node.name = stomp_desc.dest_node_name + minetest.set_node(pos, new_node) + end +end + + +--[[ +-- Revert timer, used in node registration. +--]] + +function poschangelib.change_node(pos, stomped_node_name, reverted_node_name) + -- Check if the node is still the right one + local node = minetest.get_node(pos) + if (node.name ~= stomped_node_name) then return end + -- Replace it while keeping param, param2 and other things + node.name = reverted_node_name + minetest.set_node(pos, node) +end + +--[[ +-- Node registration +--]] + +--- Set the default values for a stomp_desc. +-- stomp_desc.dest_node_name must be set. +local function stomp_desc_defaults(source_node_name, stomp_desc) + if not stomp_desc.chance then stomp_desc.chance = 1 end + if not stomp_desc.source_node then + stomp_desc.source_node = source_node_name + end + if not stomp_desc.priority then stomp_desc.priority = 100 end + if stomp_desc.duration then + stomp_desc.duration_min = stomp_desc.duration + stomp_desc.duration_max = stomp_desc.duration + stomp_desc.duration = nil + end + if not stomp_desc.priority then stomp_desc.priority = 100 end + if not stomp_desc.name then + stomp_desc.name = get_stomp_name(source_node_name, stomp_desc.dest_node_name) + end +end + +--- Register a footprinted version of a node +function poschangelib.register_footprints(node_name, stomp_desc) + if type(node_name) == 'table' then + -- Register all nodes from the table + local names = {} + for i, name in pairs(node_name) do + table.insert(names, poschangelib.register_footprints(name, stomp_desc)) + end + return names + end + -- Single node registration + local desc = minetest.registered_nodes[node_name] + if not desc then + minetest.log('error', 'Trying to register footprints for unknow node ' .. node_name) + return + end + local stomped_node_name = poschangelib.get_footprint_node_name(node_name) + -- Use a copy of stomp desc to keep it unchanged outside the function + local local_stomp_desc = table_copy(stomp_desc) + local_stomp_desc.dest_node_name = stomped_node_name + stomp_desc_defaults(node_name, local_stomp_desc) + local stomped_node_desc = table_copy(desc) + stomped_node_desc.description = desc.description .. ' With Footprint' + -- Add footprint on top of the node texture + local footprint_texture = 'poschangelib_footprint.png' + if local_stomp_desc.footprint_texture then + footprint_texture = local_stomp_desc.footprint_texture + end + if type(desc.tiles[1]) == 'table' then + -- Replace top texture + stomped_node_desc.tiles[1].name = desc.tiles[1].name .. '^' .. footprint_texture + else + -- Put footprints on top and keep the original texture for the rest + stomped_node_desc.tiles[1] = desc.tiles[1] .. '^' .. footprint_texture + stomped_node_desc.tiles[2] = desc.tiles[1] + end + -- Revert timer + if local_stomp_desc.duration_min then + if not desc.on_timer then + stomped_node_desc.on_timer = function(pos, elapsed) + poschangelib.change_node(pos, stomped_node_name, node_name) + end + end + if desc.on_construct then + stomped_node_desc.on_construct = function(pos) + desc.on_construct(pos) + minetest.get_node_timer(pos):start(math.random(local_stomp_desc.duration_min, local_stomp_desc.duration_max)) + end + else + stomped_node_desc.on_construct = function(pos) minetest.get_node_timer(pos):start(math.random(local_stomp_desc.duration_min, local_stomp_desc.duration_max)) end + end + end + -- Drop the original node when dug + if not desc.drop then + stomped_node_desc.drop = node_name + end + -- Register + minetest.register_node(stomped_node_name, stomped_node_desc) + poschangelib.register_stomp(node_name, stomped_node_name, local_stomp_desc) + -- Stomp to itself to reset the timer on restomp + poschangelib.register_stomp(stomped_node_name, stomped_node_name, local_stomp_desc) + return stomped_node_name +end + +--- Register a stomped node that has a chance to be transformed from the source. +-- @param source_node_name The name of the node before it is stomped +-- @param stomp chance Inverted chance that the source node is stomped on walking. +-- One of X. +-- @param stomp_node_name The name of the node after it is stomped +function poschangelib.register_stomp(source_node_name, stomp_node_name, stomp_desc) + if type(stomp_node_name) == 'function' and not stomp_desc.name then + minetest.log('error', 'No stomp name given with a function for ' .. source_node_name) + return + end + if type(source_node_name) == 'table' then + for i, node_name in ipairs(source_node_name) do + poschangelib.register_stomp(node_name, stomp_node_name, stomp_desc) + end + return + end + if not stomps[source_node_name] then + stomps[source_node_name] = {} + end + local local_stomp_desc = table_copy(stomp_desc) + local_stomp_desc.dest_node_name = stomp_node_name + stomp_desc_defaults(source_node_name, local_stomp_desc) + -- Insert in stomps + local inserted = false + local i = 1 + -- insert while keeping ascending priority order + while i <= #stomps[source_node_name] and not inserted do + if stomps[source_node_name][i].priority > local_stomp_desc.priority then + table.insert(stomps[source_node_name], i, local_stomp_desc) + inserted = true + end + i = i + 1 + end + -- not inserted: there is no other stomp for this node, insert it. + if not inserted then table.insert(stomps[source_node_name], local_stomp_desc) end + poschangelib.add_player_walk_listener(local_stomp_desc.name, walk_listener, {source_node_name}) +end + +-- Manually trigger an stomp if it exists and if the chance test passes. +-- @return False if no stomp is registered, true otherwise. +function poschangelib.trigger_stomp(player, pos_to_stomp, chance_factor) + local node = minetest.get_node(pos_to_stomp) + local node_desc = minetest.registered_nodes[node.name] + if not node_desc or not stomps[node.name] or #stomps[node.name] == 0 then + return false + end + local stomp_desc = stomps[node.name] + if not stomp_desc then + return false + end + if chance_factor == nil then chance_factor = 1.0 end + for i, s_desc in ipairs(stomp_desc) do + if (math.random() * s_desc.chance) < (1.0 * chance_factor) then + poschangelib.do_stomp(player, pos_to_stomp, node, node_desc, s_desc) + return true + end + end +end + +minetest.register_chatcommand('stomp', { + func = function(name, param) + local player = minetest.get_player_by_name(name) + if not player then return false, 'Player not found' end + if not minetest.check_player_privs(player, {server=true}) then return false, 'Stomp requires server privileges' end + local pos = player:get_pos() + local node_pos = {['x'] = pos.x, ['y'] = pos.y - 1, ['z'] = pos.z} + local node = minetest.get_node(node_pos) + local node_desc = minetest.registered_nodes[node.name] + if not node_desc then return end -- unknown node + if not stomps[node.name] or #stomps[node.name] == 0 then + return false, 'No stomping data found for ' .. node.name + elseif #stomps[node.name] > 1 then + local num = tonumber(param) + if num and num > 0 and num <= #stomps[node.name] then + poschangelib.do_stomp(player, node_pos, node, node_desc, stomps[node.name][num]) + return true + end + local local_stomps = stomps[node.name] + minetest.chat_send_player(name, 'Multiple stomping data found for ' .. node.name) + minetest.chat_send_player(name, 'Use /stomp X to choose which one to trigger.') + for i, v in ipairs(local_stomps) do + minetest.chat_send_player(name, ' ' .. i .. ') ' .. local_stomps[i].name) + end + return false + else + poschangelib.do_stomp(player, node_pos, node, node_desc, stomps[node.name][1]) + return true + end + end, +}) + diff --git a/textures/poschangelib_footprint.png b/textures/poschangelib_footprint.png new file mode 100644 index 0000000..c81bf02 Binary files /dev/null and b/textures/poschangelib_footprint.png differ