cmi-cd2025/init.lua

425 lines
13 KiB
Lua

--- The Common Mob Interface
-- @module cmi
-- @author raymoo
cmi = {}
--- Types.
-- The various data structures used in the API.
-- @section types
--- Object Identifiers.
-- @string type Either "player" or "mob"
-- @string identifier For players, is a player name. For mobs, is a unique ID.
-- @table Id
--- Punch callbacks.
-- @tparam ObjectRef mob
-- @tparam ?ObjectRef hitter
-- @number time_from_last_punch
-- @tab tool_capabilities
-- @tparam ?vector dir
-- @number damage
-- @tparam ?Id attacker Any indirect owner of the punch, for example a
-- player who fired an arrow.
-- @function PunchCallback
--- Reasons a mob could die.
-- The type field determines what kind of cause a @{DeathCause} is. It can be one
-- of those specified here, or a custom one provided by a mod. For custom types,
-- the fields should be specified by the mod introducing it.
-- @string type The predefined types are "punch" and "environment".
-- @tparam ?ObjectRef puncher If type == "punch", contains the puncher. The
-- puncher can be nil.
-- @tparam ?Id attacker If type == "punch", contains the attacker if it exists
-- and is known.
-- @tparam ?vector pos If type == "environment", is the position of the damaging
-- node.
-- @tparam ?node node If type == "environment", describes the damaging node.
-- @table DeathCause
--- Death callbacks.
-- @tparam ObjectRef mob the dying mob
-- @tparam DeathCause cause cause of death
-- @function DeathCallback
--- Activation callbacks.
-- @tparam ObjectRef mob the mob being activated
-- @number dtime the time since the mob was unloaded
-- @function ActivationCallback
--- Step callbacks.
-- @tparam ObjectRef mob
-- @number dtime
-- @function StepCallback
--- Component definitions.
-- @string name a unique name for the component, prefixed with the mod name
-- @func initialize a function taking no arguments and returning a new instance
-- of the data
-- @func serialize a function taking your component's data as an input and
-- returning it serialized as a string
-- @func deserialize a function taking the serialized form of your data and
-- turning it back into the original data
-- @table ComponentDef
-- Returns a table and the registration callback for it
local function make_callback_table()
local callbacks = {}
local function registerer(entry)
table.insert(callbacks, entry)
end
return callbacks, registerer
end
-- Returns a notification function
local function make_notifier(cb_table)
return function(...)
for i, cb in ipairs(cb_table) do
cb(...)
end
end
end
--- Callback Registration.
-- Functions for registering mob callbacks.
-- @section callbacks
--- Register a callback to be run when a mob is punched.
-- @tparam PunchCallback func
-- @function register_on_punchmob
local punch_callbacks
punch_callbacks, cmi.register_on_punchmob = make_callback_table()
--- Register a callback to be run when a mob dies.
-- @tparam DeathCallback func
-- @function register_on_diemob
local die_callbacks
die_callbacks, cmi.register_on_diemob = make_callback_table()
--- Register a callback to be run when a mob is activated.
-- @tparam ActivationCallback func
-- @function register_on_activatemob
local activate_callbacks
activate_callbacks, cmi.register_on_activatemob = make_callback_table()
--- Register a callback to be run on mob step.
-- @tparam StepCallback func
-- @function register_on_stepmob
local step_callbacks
step_callbacks, cmi.register_on_stepmob = make_callback_table()
--- Querying.
-- Functions for getting information about mobs.
-- @section misc
-- Wraps an entity-accepting function to accept entities or ObjectRefs.
local function on_entity(name, func)
return function(object, ...)
local o_type = type(object)
-- luaentities are tables
if o_type == "table" then
return func(object, ...)
end
if o_type == "userdata" then
local ent = object:get_luaentity()
return ent and func(ent, ...)
end
-- If no error, it's still possible that the input was bad.
error("Non-luaentity Non-ObjectRef input to" .. name)
end
end
-- Same as on_entity but for ObjectRefs
local function on_object(name, func)
return on_entity(function(ent, ...)
return func(ent.object, ...)
end)
end
--- Checks if an object is a mob.
-- @tparam ObjectRef|luaentity object
-- @treturn bool true if the object is a mob, otherwise returns a falsey value
-- @function is_mob
cmi.is_mob = on_entity("is_mob", function(ent)
return ent._cmi_is_mob
end)
--- Gets a player-readable mob name.
-- @tparam ObjectRef|luaentity object
-- @treturn string
-- @function get_mob_description
cmi.get_mob_description = on_entity("get_mob_description", function(mob)
local desc = mob.description
if desc then return desc end
local name = mob.name
local colon_pos = string.find(name, ":")
if colon_pos then
return string.sub(name, colon_pos + 1)
else
return name
end
end)
--- Health-related.
-- Functions related to hurting or healing mobs.
-- @section health
--- Attack a mob.
-- Functions like the punch method of ObjectRef, but takes an additional optional
-- argument for an indirect attacker. Also works on non-mob entities that define
-- an appropriate _cmi_attack method.
-- @tparam ObjectRef|luaentity mob
-- @tparam ObjectRef puncher
-- @number time_from_last_punch
-- @tab tool_capabilities
-- @tparam vector direction
-- @tparam ?Id attacker An indirect owner of the punch. For example, the player
-- who fired an arrow that punches the mob.
-- @function attack
local function attack_ent(mob, puncher, time_from_last_punch, tool_capabilities,
direction, attacker)
-- It's a method in the mob but I don't want to index it twice
local atk = mob._cmi_attack
if not atk then mob.object:punch(puncher, time_from_last_punch,
tool_capabilities, direction)
else
atk(mob, puncher, time_from_last_punch, tool_capabilities,
direction, attacker)
end
end
cmi.attack = on_entity("attack", attack_ent)
local function bound(x, minb, maxb)
if x < minb then
return minb
elseif x > maxb then
return maxb
else
return x
end
end
--- Punch damage calculator.
-- By default, this just calculates damage in the vanilla way. Switch it out for
-- something else to change the default damage mechanism for mobs.
-- @tparam ObjectRef mob
-- @tparam ?ObjectRef puncher
-- @tparam number time_from_last_punch
-- @tparam table tool_capabilities
-- @tparam ?vector direction
-- @tparam ?Id attacker
-- @treturn number The calculated damage
function cmi.damage_calculator(mob, puncher, tflp, caps, direction, attacker)
local a_groups = mob:get_armor_groups() or {}
local full_punch_interval = caps.full_punch_interval or 1.4
local time_prorate = bound(tflp / full_punch_interval, 0, 1)
local damage = 0
for group, damage_rating in pairs(caps.damage_groups or {}) do
local armor_rating = a_groups[group] or 0
damage = damage + damage_rating * (armor_rating / 100)
end
return math.floor(damage * time_prorate)
end
--- Components.
-- Components are data stored in a mob, that every mob is guaranteed to contain.
-- You can use them in conjunction with callbacks to extend mobs with new
-- functionality, without explicit support from mob mods.
-- @section components
--- Register a mob component.
-- @tparam ComponentDef component_def
-- @function register_component
local component_defs
component_defs, cmi.register_component = make_callback_table()
--- Get a component from a mob.
-- @tparam mob ObjectRef|luaentity mob
-- @string component_name
-- @return The requested component, or nil if it doesn't exist
-- @function get_mob_component
cmi.get_mob_component = on_entity("get_mob_component", function(mob, c_name)
return mob._cmi_components.components[c_name]
end)
--- Set a component in a mob.
-- @tparam mob ObjectRef|luaentity mob
-- @string component_name
-- @param new_value
-- @function set_mob_component
cmi.set_mob_component = on_entity("set_mob_component", function(mob, c_name, new)
mob._cmi_components.components[c_name] = new
end)
--- Unique Ids.
-- Every mob gets a unique ID when they are created. This feature is implemented
-- as a component, so you can use it as an example.
-- @section uids
local function show_hex(str)
local len = #str
local results = {}
for i = 1, len do
table.insert(results, string.format("%x", str:byte(i)))
end
return table.concat(results)
end
-- This is an ID that will be (probabilistically) unique to this session.
local session_id = SecureRandom() and show_hex(SecureRandom():next_bytes(16))
-- Fallback to math.rand with a warning
if not session_id then
minetest.log("warning",
"[cmi] SecureRandom() failed, falling back to math.random for unique IDs")
-- Generate 16 1-byte numbers, stringify them, then join them together
local id_pieces = {}
for i=1, 16 do
table.insert(id_pieces, tostring(math.rand(0, 255)))
end
session_id = table.concat(id_pieces, "-")
end
-- A unique ID is generated by appending a counter to the session ID.
local counter = 0
local function generate_id()
counter = counter + 1
return session_id .. "-" .. counter
end
cmi.register_component({
name = "cmi:uid",
initialize = generate_id,
serialize = function (x) return x end,
deserialize = function (x) return x end,
})
--- Get the unique ID of a mob.
-- @tparam ObjectRef|luaentity mob
-- @treturn string
function cmi.get_uid(mob)
return cmi.get_mob_component(mob, "cmi:uid")
end
--- Implementation: event notification.
-- Functions used to notify CMI when things happen to your mob. Only necessary
-- when you are implementing the interface.
-- @section impl_events
--- Notify CMI that your mob has been punched.
-- Call this before doing any punch handling that is not "read-only".
-- @tparam ObjectRef mob
-- @tparam ?ObjectRef hitter
-- @number time_from_last_punch
-- @tab tool_capabilities
-- @tparam ?vector dir
-- @number damage
-- @tparam ?Id attacker
-- unknown.
-- @return Returns true if punch handling should be aborted.
-- @function notify_punch
cmi.notify_punch = make_notifier(punch_callbacks)
--- Notify CMI that your mob has died.
-- Call this right before calling remove.
-- @tparam ObjectRef mob the dying mob
-- @tparam DeathCause cause cause of death
-- @function notify_die
cmi.notify_die = make_notifier(die_callbacks)
--- Notify CMI that your mob has been activated.
-- Call this after all other mob initialization.
-- @tparam ObjectRef mob the mob being activated
-- @number dtime the time since the mob was unloaded
-- @function notify_activate
cmi.notify_activate = make_notifier(activate_callbacks)
--- Notify CMI that your mob is taking a step.
-- Call this on every step. It is suggested to call it before or after all other
-- processing, to avoid logic errors caused by callbacks handling the same state
-- as your entity's normal step logic.
-- @tparam ObjectRef mob
-- @number dtime
-- @function notify_step
cmi.notify_step = make_notifier(step_callbacks)
--- Implementation: components.
-- Functions related to implementing entity components. Only necessary when you
-- are implementing the interface.
-- @section impl_components
--- Activates component data.
-- On mob activation, call this and put the result in the _cmi_components field of
-- its luaentity.
-- @tparam ?string serialized_data the serialized form of the string, if
-- available. If the mob has never had component data, do not pass this argument.
-- @return component data
function cmi.activate_components(serialized_data)
local serial_table = serialized_data and minetest.parse_json(serialized_data) or {}
local components = {}
for i = 1, #component_defs do
local def = component_defs[i]
local name = def.name
local serialized = serial_table[name]
components[name] = serialized
and def.deserialize(serialized)
or def.initialize()
end
return {
components = components,
old_serialization = serial_table,
}
end
--- Serialized component data.
-- When serializing your mob data, call this and put the result somewhere safe,
-- where it can be retrieved on activation to be passed to
-- #{activate_components}.
-- @param component_data
-- @treturn string
function cmi.serialize_components(component_data)
local serial_table = component_data.old_serialization
local components = component_data.components
for i = 1, #component_defs do
local def = component_defs[i]
local name = def.name
local component = components[name]
serial_table[name] = def.serialize(component)
end
return minetest.write_json(serial_table)
end
--- Implementation: health.
-- Functions related to health that are needed for implementation of the
-- interface. Only necessary if you are implementing the interface.
-- @section impl_damage
--- Calculate damage.
-- Use this function when you want to calculate the "default" damage. If you
-- are a modder who wants to switch out the damage mechanism, do not replace
-- this function. Replace #{damage_calculator} instead.
-- @tparam ObjectRef mob
-- @tparam ?ObjectRef puncher
-- @tparam number time_from_last_punch
-- @tparam table tool_capabilities
-- @tparam ?vector direction
-- @tparam ?Id attacker
-- @treturn number
function cmi.calculate_damage(...)
return cmi.damage_calculator(...)
end