EinsDreiDreiSieben/mods/leads/api.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;