453 lines
15 KiB
Lua
453 lines
15 KiB
Lua
--[[
|
|
Leads — Adds leads for transporting animals to Minetest.
|
|
Copyright © 2023, Silver Sandstone <@SilverSandstone@craftodon.social>
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a
|
|
copy of this software and associated documentation files (the "Software"),
|
|
to deal in the Software without restriction, including without limitation
|
|
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
and/or sell copies of the Software, and to permit persons to whom the
|
|
Software is furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in
|
|
all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
DEALINGS IN THE SOFTWARE.
|
|
]]
|
|
|
|
|
|
--- Public API functions.
|
|
-- @module api
|
|
|
|
|
|
local S = leads.S;
|
|
|
|
|
|
--- An enumerator of object types.
|
|
leads.ObjectType =
|
|
{
|
|
PLAYER = 'player';
|
|
ANIMAL = 'animal';
|
|
MONSTER = 'monster';
|
|
NPC = 'npc';
|
|
VEHICLE = 'vehicle';
|
|
OTHER = 'other';
|
|
};
|
|
|
|
|
|
--- Overrides the leashable property of entities.
|
|
leads.custom_leashable_entities =
|
|
{
|
|
['boats:boat'] = true;
|
|
};
|
|
|
|
--- Overrides the object type of entities.
|
|
leads.custom_object_types =
|
|
{
|
|
['boats:boat'] = leads.ObjectType.VEHICLE;
|
|
};
|
|
|
|
--- Overrides the knottable property of nodes.
|
|
leads.custom_knottable_nodes =
|
|
{
|
|
['ferns:fern_trunk'] = true;
|
|
['ethereal:bamboo'] = true;
|
|
['bambooforest:bamboo'] = true;
|
|
['advtrains:signal_off'] = true;
|
|
['advtrains:signal_on'] = true;
|
|
['advtrains:retrosignal_off'] = true;
|
|
['advtrains:retrosignal_on'] = true;
|
|
['nodes_nature:mahal'] = true;
|
|
['hades_furniture:binding_rusty_bars'] = true;
|
|
};
|
|
|
|
--- A table of sound effects for lead events.
|
|
leads.sounds =
|
|
{
|
|
attach = {name = 'leads_attach', gain = 0.5, pitch = 0.75};
|
|
remove = {name = 'leads_remove', gain = 0.5, pitch = 0.75};
|
|
stretch = {name = 'leads_stretch', gain = 0.25, pitch = 1.25, duration = 2.5};
|
|
snap = {name = 'leads_break', gain = 0.75};
|
|
};
|
|
|
|
|
|
local weak_key_mt = {__mode = 'k'};
|
|
local leads_by_connector_mt =
|
|
{
|
|
__mode = 'k';
|
|
__index = function(self, key)
|
|
local result = setmetatable({}, weak_key_mt);
|
|
self[key] = result;
|
|
return result;
|
|
end;
|
|
};
|
|
leads.leads_by_connector = setmetatable({}, leads_by_connector_mt);
|
|
|
|
|
|
--- Creates a lead between two objects.
|
|
-- @param leader [ObjectRef] The leader object.
|
|
-- @param follower [ObjectRef] The follower object.
|
|
-- @param item [string|ItemStack|nil] The lead item, if any.
|
|
-- @return [ObjectRef|nil] The lead object, or nil on failure.
|
|
-- @return [string|nil] A string describing the error, or nil on success.
|
|
function leads.connect_objects(leader, follower, item)
|
|
if leads.util.is_same_object(leader, follower) then
|
|
return nil, S'You cannot leash something to itself.';
|
|
end;
|
|
|
|
item = ItemStack(item);
|
|
local item_def = item:get_definition();
|
|
|
|
local l_pos = leader:get_pos();
|
|
local f_pos = follower:get_pos();
|
|
|
|
if leads.settings.debug then
|
|
minetest.log(debug.traceback(('[Leads] Connecting L:%s to F:%s.'):format(leads.util.describe_object(leader), leads.util.describe_object(follower))));
|
|
end;
|
|
|
|
local centre = (l_pos + f_pos) / 2;
|
|
|
|
local object = minetest.add_entity(centre, 'leads:lead');
|
|
if not object then
|
|
return nil, S'Failed to create lead.';
|
|
end;
|
|
|
|
local entity = object:get_luaentity();
|
|
entity.leader = leader;
|
|
entity.follower = follower;
|
|
entity:set_item(item);
|
|
entity:update_visuals();
|
|
entity:update_objref_ids();
|
|
entity:notify_connector_added(leader, true);
|
|
entity:notify_connector_added(follower, false);
|
|
minetest.sound_play(leads.sounds.attach, {pos = centre}, true);
|
|
return object, nil;
|
|
end;
|
|
|
|
|
|
--- Checks if the object can be attached to a lead.
|
|
-- @param object [ObjectRef] The object to check.
|
|
-- @return [boolean] true if the object can be attached to a lead.
|
|
function leads.is_leashable(object)
|
|
-- All entities allowed in settings:
|
|
if leads.settings.allow_leash_all then
|
|
return true;
|
|
end;
|
|
|
|
-- Check settings:
|
|
local obj_type = leads.util.get_object_type(object);
|
|
if not leads.settings['allow_leash_' .. obj_type] then
|
|
return false;
|
|
end;
|
|
|
|
-- Get entity:
|
|
local entity = object:get_luaentity();
|
|
if not entity then
|
|
return obj_type == leads.ObjectType.PLAYER;
|
|
end;
|
|
|
|
-- Custom leashable:
|
|
local leashable = entity._leads_leashable or leads.custom_leashable_entities[entity.name];
|
|
if leashable ~= nil then
|
|
return leashable;
|
|
end;
|
|
|
|
-- Mobs:
|
|
return leads.util.is_mob(object);
|
|
end;
|
|
|
|
|
|
--- Checks if the node can have lead knots tied to it.
|
|
-- @param name [string] The name of a node.
|
|
-- @return [boolean] true if the node is knottable.
|
|
function leads.is_knottable(name)
|
|
local def = minetest.registered_nodes[name];
|
|
if not def then
|
|
return false;
|
|
end;
|
|
|
|
-- Custom knottable:
|
|
local knottable = def._leads_knottable or leads.custom_knottable_nodes[name];
|
|
if knottable ~= nil then
|
|
return knottable;
|
|
end;
|
|
|
|
-- Fence:
|
|
if def.drawtype == 'fencelike' or (minetest.get_item_group(name, 'fence') > 0 and not (name:match('.*:fence_rail_.*') or name:match('.*:gate_.*'))) then
|
|
return true;
|
|
end;
|
|
|
|
-- Mese post:
|
|
if name:match('.*:mese_post_.*') then
|
|
return true;
|
|
end;
|
|
|
|
-- Lord of the Test fences:
|
|
-- (These aren't in group:fence due to a bug.)
|
|
if name:match('^lottblocks:fence_.*') then
|
|
return true;
|
|
end;
|
|
|
|
return false;
|
|
end;
|
|
|
|
|
|
--- Finds a lead connected to the specified leader.
|
|
-- If there are multiple matching leads, one is chosen arbitrarily.
|
|
-- @param leader [ObjectRef] The player or entity to find leads connected to.
|
|
-- @return [ObjectRef|nil] The lead, if any.
|
|
function leads.find_lead_by_leader(leader)
|
|
for lead in leads.find_connected_leads(leader, true, false) do
|
|
return lead;
|
|
end;
|
|
return nil;
|
|
end;
|
|
|
|
|
|
--- Finds leads connected to the specified object.
|
|
-- @param connector [ObjectRef] The player or entity to find leads connected to.
|
|
-- @param accept_leader [boolean] Find leads where the specified object is the leader.
|
|
-- @param accept_follower [boolean] Find leads where the specified object is the follower.
|
|
-- @return [function] An iterator of (lead: ObjectRef, is_leader: boolean).
|
|
function leads.find_connected_leads(connector, accept_leader, accept_follower)
|
|
local function _iter()
|
|
for lead in pairs(leads.leads_by_connector[connector]) do
|
|
local entity = lead:get_luaentity();
|
|
if accept_leader and entity.leader and leads.util.is_same_object(entity.leader, connector) then
|
|
coroutine.yield(lead, true);
|
|
elseif accept_follower and entity.follower and leads.util.is_same_object(entity.follower, connector) then
|
|
coroutine.yield(lead, false);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
return coroutine.wrap(_iter);
|
|
end;
|
|
|
|
|
|
--- Ties the leader's lead to a post.
|
|
-- @param leader [ObjectRef] The leader whose lead to tie.
|
|
-- @param pos [vector] Where to tie the knot.
|
|
-- @return [ObjectRef|nil] The knot object, or nil on failure.
|
|
function leads.knot(leader, pos)
|
|
pos = vector.round(pos);
|
|
|
|
-- Check protection:
|
|
if leads.settings.respect_protection and not minetest.check_player_privs(leader, 'protection_bypass') then
|
|
local name = leader and leader:get_player_name() or '';
|
|
if minetest.is_protected(pos, name) then
|
|
minetest.record_protection_violation(pos, name);
|
|
return nil;
|
|
end;
|
|
end;
|
|
|
|
-- Find a lead attached to the player:
|
|
local lead = leads.find_lead_by_leader(leader);
|
|
if not lead then
|
|
return nil;
|
|
end;
|
|
|
|
-- Create a knot:
|
|
local knot = leads.add_knot(pos);
|
|
if not knot then
|
|
return nil;
|
|
end;
|
|
|
|
-- Play sound:
|
|
minetest.sound_play(leads.sounds.attach, {pos = pos}, true);
|
|
|
|
-- Attach the lead to the knot:
|
|
lead:get_luaentity():set_leader(knot);
|
|
return knot;
|
|
end;
|
|
|
|
|
|
--- Adds a knot on a fence post, or finds an existing one.
|
|
-- @param pos [vector] Where to tie the knot.
|
|
-- @return [ObjectRef|nil] A new or existing knot, or nil if creating the knot failed.
|
|
function leads.add_knot(pos)
|
|
pos = pos:round();
|
|
|
|
for __, object in ipairs(minetest.get_objects_in_area(pos, pos)) do
|
|
local entity = object:get_luaentity();
|
|
if entity and entity.name == 'leads:knot' then
|
|
return object;
|
|
end;
|
|
end;
|
|
|
|
return minetest.add_entity(pos, 'leads:knot');
|
|
end;
|
|
|
|
|
|
--- Checks if the specified object is immobile (cannot be moved with a lead).
|
|
-- @param object [ObjectRef|nil] The object to check.
|
|
-- @return [boolean] true if the object is immobile.
|
|
function leads.is_immobile(object)
|
|
local entity = object and object:get_luaentity();
|
|
return entity and entity._leads_immobile or false;
|
|
end;
|
|
|
|
|
|
--- Checks if the player is allowed to leash the object, according to ownership and mod settings.
|
|
-- @param object [ObjectRef|nil] The object to check.
|
|
-- @param player [ObjectRef|string|nil] The player trying to leash the object.
|
|
-- @return [boolean] true if the player is allowed to leash the object.
|
|
function leads.allowed_to_leash(object, player)
|
|
local name = '';
|
|
if player == nil then
|
|
name = '';
|
|
elseif type(player) == 'string' then
|
|
name = player;
|
|
else
|
|
name = player:get_player_name() or '';
|
|
end;
|
|
|
|
-- Players with the 'protection_bypass' privilege can bypass protection and ownership:
|
|
if minetest.check_player_privs(name, 'protection_bypass') then
|
|
return true;
|
|
end;
|
|
|
|
-- Players can always leash their own animals:
|
|
local owner = leads.util.get_object_owner(object);
|
|
if owner == name then
|
|
return true;
|
|
end;
|
|
|
|
-- Players can't leash anything else in protected areas if protection support is enabled:
|
|
if leads.settings.respect_protection then
|
|
local pos = object:get_pos():round();
|
|
if minetest.is_protected(pos, name) then
|
|
minetest.record_protection_violation(pos, name);
|
|
return false;
|
|
end;
|
|
end;
|
|
|
|
-- Otherwise, use the appropriate setting:
|
|
if owner == '' then
|
|
return leads.settings.allow_leash_unowned or not leads.util.is_mob(object);
|
|
else
|
|
return leads.settings.allow_leash_owned_other;
|
|
end;
|
|
end;
|
|
|
|
|
|
--- Implements lead item use.
|
|
-- @param itemstack [ItemStack] The player's held item.
|
|
-- @param user [ObjectRef] The player using the lead.
|
|
-- @param pointed_thing [PointedThing] The pointed-thing.
|
|
-- @param is_punch [boolean] true if the interaction is a punch.
|
|
-- @return [ItemStack|nil] The leftover itemstack, or nil for no change.
|
|
function leads.on_lead_interact(itemstack, user, pointed_thing, is_punch)
|
|
local function _message(message)
|
|
if leads.settings.chat_messages then
|
|
minetest.chat_send_player(user:get_player_name(), message);
|
|
end;
|
|
end;
|
|
|
|
|
|
if pointed_thing.under then
|
|
-- Clicking on a node:
|
|
local pos = pointed_thing.under;
|
|
local node = minetest.get_node(pos);
|
|
if not leads.is_knottable(node.name) then
|
|
return nil;
|
|
end;
|
|
|
|
-- Check protection:
|
|
if leads.settings.respect_protection and not minetest.check_player_privs(user, 'protection_bypass') then
|
|
local name = user and user:get_player_name() or '';
|
|
if minetest.is_protected(pos, name) then
|
|
minetest.record_protection_violation(pos, name);
|
|
return nil;
|
|
end;
|
|
end;
|
|
|
|
-- Create new lead with knot:
|
|
local knot = leads.add_knot(pos);
|
|
if not knot then
|
|
return nil;
|
|
end;
|
|
leads.connect_objects(user, knot, itemstack:peek_item());
|
|
|
|
else
|
|
-- Clicking on an object:
|
|
local object = pointed_thing.ref;
|
|
if not object then
|
|
return nil;
|
|
end;
|
|
|
|
-- Try the entity's custom lead interact callback:
|
|
local entity = object:get_luaentity();
|
|
if entity and entity._leads_on_interact then
|
|
local override, result = entity:_leads_on_interact(itemstack, user, pointed_thing, is_punch);
|
|
if override then
|
|
return result;
|
|
end;
|
|
end;
|
|
|
|
-- The player right-clicked on a knot — try knotting their lead before making a new one:
|
|
if entity and entity.name == 'leads:knot' then
|
|
if leads.knot(user, object:get_pos()) then
|
|
return nil;
|
|
end;
|
|
end;
|
|
|
|
-- Make sure the object is leashable:
|
|
if not leads.is_leashable(object) then
|
|
_message(S'You cannot leash this.');
|
|
return nil;
|
|
end;
|
|
|
|
-- Make sure the player is allowed to leash the object:
|
|
if not leads.allowed_to_leash(object, user) then
|
|
_message(S'You do not own this.');
|
|
return nil;
|
|
end;
|
|
|
|
-- Create the lead:
|
|
local lead, message = leads.connect_objects(user, pointed_thing.ref, itemstack:peek_item());
|
|
if not lead then
|
|
_message(message);
|
|
return nil;
|
|
end;
|
|
end;
|
|
|
|
-- Consume the lead item:
|
|
if not (minetest.is_player(user) and minetest.is_creative_enabled(user:get_player_name())) then
|
|
itemstack:take_item(1);
|
|
end;
|
|
return itemstack;
|
|
end;
|
|
|
|
|
|
--- The `on_secondary_use`/`on_rightclick` handler for lead items.
|
|
-- @param itemstack [ItemStack] The player's held item.
|
|
-- @param user [ObjectRef] The player using the lead.
|
|
-- @param pointed_thing [PointedThing] The pointed-thing.
|
|
-- @return [ItemStack|nil] The leftover itemstack, or nil for no change.
|
|
function leads.on_lead_use(itemstack, user, pointed_thing)
|
|
local result = leads.on_lead_interact(itemstack, user, pointed_thing, false);
|
|
if (not result) and pointed_thing.under then
|
|
-- Fallback to the node's right-click handler:
|
|
local node = minetest.get_node(pointed_thing.under);
|
|
local def = minetest.registered_nodes[node.name] or {};
|
|
return def.on_rightclick and def.on_rightclick(pointed_thing.under, node, user, itemstack, pointed_thing) or nil;
|
|
end;
|
|
return result;
|
|
end;
|
|
|
|
|
|
--- The `on_use` handler for lead items.
|
|
-- @param itemstack [ItemStack] The player's held item.
|
|
-- @param user [ObjectRef] The player using the lead.
|
|
-- @param pointed_thing [PointedThing] The pointed-thing.
|
|
-- @return [ItemStack|nil] The leftover itemstack, or nil for no change.
|
|
function leads.on_lead_punch(itemstack, user, pointed_thing)
|
|
return leads.on_lead_interact(itemstack, user, pointed_thing, true);
|
|
end;
|