From 2575c1012146df3f5813d819637e1bfd4edb2e2f Mon Sep 17 00:00:00 2001 From: OldCoder Date: Sun, 4 Sep 2022 22:03:05 -0700 Subject: [PATCH] Imported from trollstream "ContentDB" --- LICENSE | 14 ++ README.md | 275 ++++++++++++++++++++++++ init.lua | 322 ++++++++++++++++++++++++++++ mod.conf | 1 + register.lua | 64 ++++++ settingtypes.txt | 8 + stomping.lua | 286 ++++++++++++++++++++++++ textures/poschangelib_footprint.png | Bin 0 -> 131 bytes 8 files changed, 970 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 init.lua create mode 100644 mod.conf create mode 100644 register.lua create mode 100644 settingtypes.txt create mode 100644 stomping.lua create mode 100644 textures/poschangelib_footprint.png 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 0000000000000000000000000000000000000000..c81bf02fc2bfd0fa8d1e78a797a029adc4c5c219 GIT binary patch literal 131 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`UY;(FAr_~T6C^SYa3tTjdGn^S zw}rK}@s;b>$?c#O#*fedL*x?JzJx4SSi67NDa;rtVi#^ih e5p%P77o%A&=h}nk57z=sW$<+Mb6Mw<&;$UJMk?