361 lines
11 KiB
Lua
361 lines
11 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.
|
|
]]
|
|
|
|
|
|
--- Generic utility functions.
|
|
-- @module util
|
|
|
|
|
|
leads.util = {};
|
|
|
|
leads.util.rng = PseudoRandom(0x4C656164);
|
|
|
|
local has_objectuuids = minetest.get_modpath('objectuuids') ~= nil;
|
|
|
|
|
|
--- Checks if the object is a mob.
|
|
-- @param object [ObjectRef] The object to check.
|
|
-- @return [boolean] true if the object is a mob.
|
|
function leads.util.is_mob(object)
|
|
local entity = object:get_luaentity();
|
|
if not entity then
|
|
return false;
|
|
end;
|
|
|
|
-- Explicitly marked as an animal:
|
|
local result = entity._leads_is_mob or entity._leads_is_animal;
|
|
if result ~= nil then
|
|
return result;
|
|
end;
|
|
|
|
-- Mobs (Redo) and Repixture:
|
|
if entity.health then
|
|
return true;
|
|
end;
|
|
|
|
-- Creatura:
|
|
if entity._creatura_mob then
|
|
return true;
|
|
end;
|
|
|
|
-- Exile:
|
|
if entity.hp and (entity.max_health or entity.max_hp) then
|
|
return true;
|
|
end;
|
|
|
|
return false;
|
|
end;
|
|
|
|
leads.util.is_animal = leads.util.is_mob; -- Deprecated alias.
|
|
|
|
|
|
--- Tiles a texture to the specified size.
|
|
-- @param texture [string] The texture to tile.
|
|
-- @param src_width [integer] The input texture's width.
|
|
-- @param src_height [integer] The input texture's height.
|
|
-- @param out_width [integer] The resulting texture's width.
|
|
-- @param out_height [integer] The resulting texture's height.
|
|
-- @return [string] A texture string.
|
|
function leads.util.tile_texture(texture, src_width, src_height, out_width, out_height)
|
|
texture = leads.util.escape_texture(('(%s)^[resize:%dx%d'):format(texture, src_width, src_height));
|
|
local parts = {'[combine:', out_width, 'x', out_height};
|
|
local y = 0;
|
|
while y < out_height do
|
|
local x = 0;
|
|
while x < out_width do
|
|
table.insert(parts, (':%d,0=%s'):format(x, texture));
|
|
x = x + src_width;
|
|
end;
|
|
y = y + src_height;
|
|
end;
|
|
return table.concat(parts, '');
|
|
end;
|
|
|
|
|
|
--- Escapes a texture for use with [combine.
|
|
-- @param texture [string] A texture string.
|
|
-- @return [string] An escaped texture string.
|
|
function leads.util.escape_texture(texture)
|
|
return string.gsub(texture, '[\\^:]', function(char) return '\\' .. char; end);
|
|
end;
|
|
|
|
|
|
--- Serialises the identity (not the state) of an object reference.
|
|
-- @param obj [ObjectRef|nil] The object to serialise.
|
|
-- @return [table|nil] A table identifying the object, or nil if the reference is invalid.
|
|
function leads.util.serialise_objref(obj)
|
|
if not obj then
|
|
return nil;
|
|
end;
|
|
|
|
local result = {pos = obj:get_pos()};
|
|
|
|
if has_objectuuids then
|
|
result.uuid = objectuuids.get_uuid(obj);
|
|
end;
|
|
|
|
if minetest.is_player(obj) then
|
|
result.player_name = obj:get_player_name();
|
|
else
|
|
local entity = obj:get_luaentity();
|
|
if not entity then
|
|
return nil;
|
|
end;
|
|
result.name = entity.name;
|
|
end;
|
|
|
|
return result;
|
|
end;
|
|
|
|
|
|
--- Deserialises an object ID previously returned from `serialise_objref()`, trying to identify the original object.
|
|
-- @param id [table|nil] A table identifying an object.
|
|
-- @return [ObjectRef|nil] An object matching the ID, or nil if no such object was found.
|
|
function leads.util.deserialise_objref(id)
|
|
if not id then
|
|
return nil;
|
|
end;
|
|
|
|
-- Objects are identified by UUID where possible:
|
|
if has_objectuuids and id.uuid then
|
|
return objectuuids.get_object_by_uuid(id.uuid);
|
|
end;
|
|
|
|
-- Without UUIDs, players are identified by name:
|
|
if id.player_name then
|
|
return minetest.get_player_by_name(id.player_name);
|
|
end;
|
|
|
|
-- Minetest doesn't provide any way to persistently identify Lua entities,
|
|
-- so the best we can do is look for an entity with the correct name near
|
|
-- the saved position.
|
|
if not id.pos then
|
|
return nil;
|
|
end;
|
|
local pos = vector.new(id.pos);
|
|
|
|
local range = 3;
|
|
local range_min = pos:offset(-range, -range, -range);
|
|
local range_max = pos:offset( range, range, range);
|
|
local objects = minetest.get_objects_in_area(range_min, range_max);
|
|
local best_object = nil;
|
|
local best_distance = math.huge;
|
|
for __, object in ipairs(objects) do
|
|
local entity = object:get_luaentity();
|
|
if entity and (id.name == nil or entity.name == id.name) then
|
|
local distance = object:get_pos():distance(pos);
|
|
if distance <= 0.0 then
|
|
return object;
|
|
elseif distance < best_distance then
|
|
best_distance = distance;
|
|
best_object = object;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
return best_object;
|
|
end;
|
|
|
|
|
|
--- Checks if two objrefs refer to the same object, which may be a player or entity.
|
|
-- @param obj1 [ObjectRef|nil] The first object to compare.
|
|
-- @param obj2 [ObjectRef|nil] The second object to compare.
|
|
-- @return [boolean] true if obj1 and obj2 reference the same object.
|
|
function leads.util.is_same_object(obj1, obj2)
|
|
if not (obj1 and obj2) then
|
|
return false;
|
|
end;
|
|
|
|
local obj1_is_player = minetest.is_player(obj1);
|
|
local obj2_is_player = minetest.is_player(obj2);
|
|
if obj1_is_player ~= obj2_is_player then
|
|
return false;
|
|
end;
|
|
|
|
if obj1_is_player then
|
|
return obj1:get_player_name() == obj2:get_player_name();
|
|
else
|
|
return obj1:get_luaentity() == obj2:get_luaentity();
|
|
end;
|
|
end;
|
|
|
|
|
|
--- Returns the relative attachment position for the specified object.
|
|
-- @param object [ObjectRef|nil] The player or entity to check.
|
|
-- @return [vector] The attachment offset as a vector relative to the object's origin.
|
|
function leads.util.get_attach_offset(object)
|
|
local properties = object and object:get_properties();
|
|
if not properties then
|
|
return vector.zero();
|
|
end;
|
|
local hitbox = (properties.physical and properties.collisionbox) or (properties.pointable and properties.selectionbox) or {};
|
|
local bottom = hitbox[2] or 0;
|
|
local top = hitbox[5] or 0;
|
|
return vector.new(0, (bottom + top) / 2, 0);
|
|
end;
|
|
|
|
|
|
--- Finds the first item available for crafting.
|
|
-- @param ... [string] Any number of prefixed node/item IDs.
|
|
-- @return [string|nil] One of the specified IDs, or nil.
|
|
function leads.util.first_available_item(...)
|
|
for __, name in ipairs{...} do
|
|
if name == '' or string.match(name, '^group:.*') or minetest.registered_items[name] then
|
|
return name;
|
|
end;
|
|
end;
|
|
return nil;
|
|
end;
|
|
|
|
|
|
--- Returns a string describing an object, for debugging.
|
|
-- @param object [ObjectRef] An object reference.
|
|
-- @return [string] A string describing the object.
|
|
function leads.util.describe_object(object)
|
|
if minetest.is_player(object) then
|
|
return ('[Player %q]'):format(object:get_player_name());
|
|
end;
|
|
|
|
local entity = object:get_luaentity();
|
|
if entity then
|
|
return ('[LuaEntity %q]'):format(entity.name);
|
|
end;
|
|
|
|
return '[Unknown object]';
|
|
end;
|
|
|
|
|
|
--- Prevents the player from interacting for some time.
|
|
-- @param name [string] The name of the player.
|
|
-- @param time [number] How long to block interactions, in seconds.
|
|
function leads.util.block_player_interaction(name, time)
|
|
local function _callback()
|
|
leads.interaction_blockers[name] = nil;
|
|
end;
|
|
|
|
local old_timer = leads.interaction_blockers[name];
|
|
if old_timer then
|
|
old_timer:cancel();
|
|
end;
|
|
|
|
leads.interaction_blockers[name] = minetest.after(time, _callback);
|
|
end;
|
|
|
|
|
|
--- Figures out the type of an object.
|
|
-- @param object [ObjectRef] The object to check.
|
|
-- @return [ObjectType] The type of the object.
|
|
function leads.util.get_object_type(object)
|
|
-- Check player:
|
|
if minetest.is_player(object) then
|
|
return leads.ObjectType.PLAYER;
|
|
end;
|
|
|
|
-- Get entity:
|
|
local entity = object:get_luaentity();
|
|
if not entity then
|
|
return leads.ObjectType.OTHER;
|
|
end;
|
|
|
|
-- Custom type override:
|
|
local override = entity._leads_type or leads.custom_object_types[entity.name];
|
|
if override then
|
|
return override;
|
|
end;
|
|
|
|
-- Get entity definition:
|
|
local def = minetest.registered_entities[entity.name];
|
|
if not def then
|
|
return leads.ObjectType.OTHER;
|
|
end;
|
|
|
|
-- Check Creatura (assumed to be animals):
|
|
if entity._creatura_mob then
|
|
return leads.ObjectType.ANIMAL;
|
|
end;
|
|
|
|
-- Check Mobs API type:
|
|
if def.type == 'animal' then
|
|
return leads.ObjectType.ANIMAL;
|
|
elseif def.type == 'monster' then
|
|
return leads.ObjectType.MONSTER;
|
|
elseif def.type == 'npc' then
|
|
return leads.ObjectType.NPC;
|
|
end;
|
|
|
|
return leads.ObjectType.OTHER;
|
|
end;
|
|
|
|
|
|
--- Gets the owner of an object.
|
|
-- @param object [ObjectRef] The object to check.
|
|
-- @return [string] The owner's name, or '' for unowned.
|
|
function leads.util.get_object_owner(object)
|
|
local entity = object:get_luaentity();
|
|
if not entity then
|
|
return '';
|
|
end;
|
|
|
|
return entity.owner or '';
|
|
end;
|
|
|
|
|
|
--- Calculates the mass of a player or entity.
|
|
-- @param object [ObjectRef] The object to check.
|
|
-- @return [number] The object's mass, in an abstract unit.
|
|
function leads.util.get_object_mass(object)
|
|
local entity = object:get_luaentity();
|
|
|
|
local mass = entity and entity._leads_mass;
|
|
if mass then
|
|
return mass;
|
|
end;
|
|
|
|
local density = entity and entity._leads_density or 1;
|
|
local properties = object:get_properties() or {};
|
|
local hitbox = properties.collisionbox or properties.selectionbox;
|
|
if not hitbox then
|
|
return density;
|
|
end;
|
|
|
|
local width = math.abs(hitbox[4] - hitbox[1]);
|
|
local height = math.abs(hitbox[5] - hitbox[2]);
|
|
local depth = math.abs(hitbox[6] - hitbox[3]);
|
|
return width * height * depth * density;
|
|
end;
|
|
|
|
|
|
--- Clamps a value within the specified range.
|
|
-- @param value [number] The value to clamp.
|
|
-- @param min [number] The lower bound.
|
|
-- @param max [number] The upper bound.
|
|
-- @return [number] A number between lower and upper.
|
|
function leads.util.clamp(value, min, max)
|
|
if value < min then
|
|
return min;
|
|
elseif value > max then
|
|
return max;
|
|
else
|
|
return value;
|
|
end;
|
|
end;
|