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