minetest/builtin/game/register.lua
Warr1024 52128ae11e
Add API for mods to hook liquid transformation events (#11405)
Add API for mods to hook liquid transformation events

Without this API, there is no reliable way for mods to be
notified when liquid transform modifies nodes and mods are
forced to poll for changes.  This allows mods to detect
changes to flowing liquid nodes and liquid renewal using
event-driven logic.
2021-07-09 09:08:40 -04:00

620 lines
19 KiB
Lua

-- Minetest: builtin/misc_register.lua
local S = core.get_translator("__builtin")
--
-- Make raw registration functions inaccessible to anyone except this file
--
local register_item_raw = core.register_item_raw
core.register_item_raw = nil
local unregister_item_raw = core.unregister_item_raw
core.unregister_item_raw = nil
local register_alias_raw = core.register_alias_raw
core.register_alias_raw = nil
--
-- Item / entity / ABM / LBM registration functions
--
core.registered_abms = {}
core.registered_lbms = {}
core.registered_entities = {}
core.registered_items = {}
core.registered_nodes = {}
core.registered_craftitems = {}
core.registered_tools = {}
core.registered_aliases = {}
-- For tables that are indexed by item name:
-- If table[X] does not exist, default to table[core.registered_aliases[X]]
local alias_metatable = {
__index = function(t, name)
return rawget(t, core.registered_aliases[name])
end
}
setmetatable(core.registered_items, alias_metatable)
setmetatable(core.registered_nodes, alias_metatable)
setmetatable(core.registered_craftitems, alias_metatable)
setmetatable(core.registered_tools, alias_metatable)
-- These item names may not be used because they would interfere
-- with legacy itemstrings
local forbidden_item_names = {
MaterialItem = true,
MaterialItem2 = true,
MaterialItem3 = true,
NodeItem = true,
node = true,
CraftItem = true,
craft = true,
MBOItem = true,
ToolItem = true,
tool = true,
}
local function check_modname_prefix(name)
if name:sub(1,1) == ":" then
-- If the name starts with a colon, we can skip the modname prefix
-- mechanism.
return name:sub(2)
else
-- Enforce that the name starts with the correct mod name.
local expected_prefix = core.get_current_modname() .. ":"
if name:sub(1, #expected_prefix) ~= expected_prefix then
error("Name " .. name .. " does not follow naming conventions: " ..
"\"" .. expected_prefix .. "\" or \":\" prefix required")
end
-- Enforce that the name only contains letters, numbers and underscores.
local subname = name:sub(#expected_prefix+1)
if subname:find("[^%w_]") then
error("Name " .. name .. " does not follow naming conventions: " ..
"contains unallowed characters")
end
return name
end
end
function core.register_abm(spec)
-- Add to core.registered_abms
assert(type(spec.action) == "function", "Required field 'action' of type function")
core.registered_abms[#core.registered_abms + 1] = spec
spec.mod_origin = core.get_current_modname() or "??"
end
function core.register_lbm(spec)
-- Add to core.registered_lbms
check_modname_prefix(spec.name)
assert(type(spec.action) == "function", "Required field 'action' of type function")
core.registered_lbms[#core.registered_lbms + 1] = spec
spec.mod_origin = core.get_current_modname() or "??"
end
function core.register_entity(name, prototype)
-- Check name
if name == nil then
error("Unable to register entity: Name is nil")
end
name = check_modname_prefix(tostring(name))
prototype.name = name
prototype.__index = prototype -- so that it can be used as a metatable
-- Add to core.registered_entities
core.registered_entities[name] = prototype
prototype.mod_origin = core.get_current_modname() or "??"
end
function core.register_item(name, itemdef)
-- Check name
if name == nil then
error("Unable to register item: Name is nil")
end
name = check_modname_prefix(tostring(name))
if forbidden_item_names[name] then
error("Unable to register item: Name is forbidden: " .. name)
end
itemdef.name = name
-- Apply defaults and add to registered_* table
if itemdef.type == "node" then
-- Use the nodebox as selection box if it's not set manually
if itemdef.drawtype == "nodebox" and not itemdef.selection_box then
itemdef.selection_box = itemdef.node_box
elseif itemdef.drawtype == "fencelike" and not itemdef.selection_box then
itemdef.selection_box = {
type = "fixed",
fixed = {-1/8, -1/2, -1/8, 1/8, 1/2, 1/8},
}
end
if itemdef.light_source and itemdef.light_source > core.LIGHT_MAX then
itemdef.light_source = core.LIGHT_MAX
core.log("warning", "Node 'light_source' value exceeds maximum," ..
" limiting to maximum: " ..name)
end
setmetatable(itemdef, {__index = core.nodedef_default})
core.registered_nodes[itemdef.name] = itemdef
elseif itemdef.type == "craft" then
setmetatable(itemdef, {__index = core.craftitemdef_default})
core.registered_craftitems[itemdef.name] = itemdef
elseif itemdef.type == "tool" then
setmetatable(itemdef, {__index = core.tooldef_default})
core.registered_tools[itemdef.name] = itemdef
elseif itemdef.type == "none" then
setmetatable(itemdef, {__index = core.noneitemdef_default})
else
error("Unable to register item: Type is invalid: " .. dump(itemdef))
end
-- Flowing liquid uses param2
if itemdef.type == "node" and itemdef.liquidtype == "flowing" then
itemdef.paramtype2 = "flowingliquid"
end
-- BEGIN Legacy stuff
if itemdef.cookresult_itemstring ~= nil and itemdef.cookresult_itemstring ~= "" then
core.register_craft({
type="cooking",
output=itemdef.cookresult_itemstring,
recipe=itemdef.name,
cooktime=itemdef.furnace_cooktime
})
end
if itemdef.furnace_burntime ~= nil and itemdef.furnace_burntime >= 0 then
core.register_craft({
type="fuel",
recipe=itemdef.name,
burntime=itemdef.furnace_burntime
})
end
-- END Legacy stuff
itemdef.mod_origin = core.get_current_modname() or "??"
-- Disable all further modifications
getmetatable(itemdef).__newindex = {}
--core.log("Registering item: " .. itemdef.name)
core.registered_items[itemdef.name] = itemdef
core.registered_aliases[itemdef.name] = nil
register_item_raw(itemdef)
end
function core.unregister_item(name)
if not core.registered_items[name] then
core.log("warning", "Not unregistering item " ..name..
" because it doesn't exist.")
return
end
-- Erase from registered_* table
local type = core.registered_items[name].type
if type == "node" then
core.registered_nodes[name] = nil
elseif type == "craft" then
core.registered_craftitems[name] = nil
elseif type == "tool" then
core.registered_tools[name] = nil
end
core.registered_items[name] = nil
unregister_item_raw(name)
end
function core.register_node(name, nodedef)
nodedef.type = "node"
core.register_item(name, nodedef)
end
function core.register_craftitem(name, craftitemdef)
craftitemdef.type = "craft"
-- BEGIN Legacy stuff
if craftitemdef.inventory_image == nil and craftitemdef.image ~= nil then
craftitemdef.inventory_image = craftitemdef.image
end
-- END Legacy stuff
core.register_item(name, craftitemdef)
end
function core.register_tool(name, tooldef)
tooldef.type = "tool"
tooldef.stack_max = 1
-- BEGIN Legacy stuff
if tooldef.inventory_image == nil and tooldef.image ~= nil then
tooldef.inventory_image = tooldef.image
end
if tooldef.tool_capabilities == nil and
(tooldef.full_punch_interval ~= nil or
tooldef.basetime ~= nil or
tooldef.dt_weight ~= nil or
tooldef.dt_crackiness ~= nil or
tooldef.dt_crumbliness ~= nil or
tooldef.dt_cuttability ~= nil or
tooldef.basedurability ~= nil or
tooldef.dd_weight ~= nil or
tooldef.dd_crackiness ~= nil or
tooldef.dd_crumbliness ~= nil or
tooldef.dd_cuttability ~= nil) then
tooldef.tool_capabilities = {
full_punch_interval = tooldef.full_punch_interval,
basetime = tooldef.basetime,
dt_weight = tooldef.dt_weight,
dt_crackiness = tooldef.dt_crackiness,
dt_crumbliness = tooldef.dt_crumbliness,
dt_cuttability = tooldef.dt_cuttability,
basedurability = tooldef.basedurability,
dd_weight = tooldef.dd_weight,
dd_crackiness = tooldef.dd_crackiness,
dd_crumbliness = tooldef.dd_crumbliness,
dd_cuttability = tooldef.dd_cuttability,
}
end
-- END Legacy stuff
-- This isn't just legacy, but more of a convenience feature
local toolcaps = tooldef.tool_capabilities
if toolcaps and toolcaps.punch_attack_uses == nil then
for _, cap in pairs(toolcaps.groupcaps or {}) do
local level = (cap.maxlevel or 0) - 1
if (cap.uses or 0) ~= 0 and level >= 0 then
toolcaps.punch_attack_uses = cap.uses * (3 ^ level)
break
end
end
end
core.register_item(name, tooldef)
end
function core.register_alias(name, convert_to)
if forbidden_item_names[name] then
error("Unable to register alias: Name is forbidden: " .. name)
end
if core.registered_items[name] ~= nil then
core.log("warning", "Not registering alias, item with same name" ..
" is already defined: " .. name .. " -> " .. convert_to)
else
--core.log("Registering alias: " .. name .. " -> " .. convert_to)
core.registered_aliases[name] = convert_to
register_alias_raw(name, convert_to)
end
end
function core.register_alias_force(name, convert_to)
if forbidden_item_names[name] then
error("Unable to register alias: Name is forbidden: " .. name)
end
if core.registered_items[name] ~= nil then
core.unregister_item(name)
core.log("info", "Removed item " ..name..
" while attempting to force add an alias")
end
--core.log("Registering alias: " .. name .. " -> " .. convert_to)
core.registered_aliases[name] = convert_to
register_alias_raw(name, convert_to)
end
function core.on_craft(itemstack, player, old_craft_list, craft_inv)
for _, func in ipairs(core.registered_on_crafts) do
itemstack = func(itemstack, player, old_craft_list, craft_inv) or itemstack
end
return itemstack
end
function core.craft_predict(itemstack, player, old_craft_list, craft_inv)
for _, func in ipairs(core.registered_craft_predicts) do
itemstack = func(itemstack, player, old_craft_list, craft_inv) or itemstack
end
return itemstack
end
-- Alias the forbidden item names to "" so they can't be
-- created via itemstrings (e.g. /give)
for name in pairs(forbidden_item_names) do
core.registered_aliases[name] = ""
register_alias_raw(name, "")
end
--
-- Built-in node definitions. Also defined in C.
--
core.register_item(":unknown", {
type = "none",
description = S("Unknown Item"),
inventory_image = "unknown_item.png",
on_place = core.item_place,
on_secondary_use = core.item_secondary_use,
on_drop = core.item_drop,
groups = {not_in_creative_inventory=1},
diggable = true,
})
core.register_node(":air", {
description = S("Air"),
inventory_image = "air.png",
wield_image = "air.png",
drawtype = "airlike",
paramtype = "light",
sunlight_propagates = true,
walkable = false,
pointable = false,
diggable = false,
buildable_to = true,
floodable = true,
air_equivalent = true,
drop = "",
groups = {not_in_creative_inventory=1},
})
core.register_node(":ignore", {
description = S("Ignore"),
inventory_image = "ignore.png",
wield_image = "ignore.png",
drawtype = "airlike",
paramtype = "none",
sunlight_propagates = false,
walkable = false,
pointable = false,
diggable = false,
buildable_to = true, -- A way to remove accidentally placed ignores
air_equivalent = true,
drop = "",
groups = {not_in_creative_inventory=1},
node_placement_prediction = "",
on_place = function(itemstack, placer, pointed_thing)
core.chat_send_player(
placer:get_player_name(),
core.colorize("#FF0000",
S("You can't place 'ignore' nodes!")))
return ""
end,
})
-- The hand (bare definition)
core.register_item(":", {
type = "none",
wield_image = "wieldhand.png",
groups = {not_in_creative_inventory=1},
})
function core.override_item(name, redefinition)
if redefinition.name ~= nil then
error("Attempt to redefine name of "..name.." to "..dump(redefinition.name), 2)
end
if redefinition.type ~= nil then
error("Attempt to redefine type of "..name.." to "..dump(redefinition.type), 2)
end
local item = core.registered_items[name]
if not item then
error("Attempt to override non-existent item "..name, 2)
end
for k, v in pairs(redefinition) do
rawset(item, k, v)
end
register_item_raw(item)
end
core.callback_origins = {}
function core.run_callbacks(callbacks, mode, ...)
assert(type(callbacks) == "table")
local cb_len = #callbacks
if cb_len == 0 then
if mode == 2 or mode == 3 then
return true
elseif mode == 4 or mode == 5 then
return false
end
end
local ret = nil
for i = 1, cb_len do
local origin = core.callback_origins[callbacks[i]]
if origin then
core.set_last_run_mod(origin.mod)
end
local cb_ret = callbacks[i](...)
if mode == 0 and i == 1 then
ret = cb_ret
elseif mode == 1 and i == cb_len then
ret = cb_ret
elseif mode == 2 then
if not cb_ret or i == 1 then
ret = cb_ret
end
elseif mode == 3 then
if cb_ret then
return cb_ret
end
ret = cb_ret
elseif mode == 4 then
if (cb_ret and not ret) or i == 1 then
ret = cb_ret
end
elseif mode == 5 and cb_ret then
return cb_ret
end
end
return ret
end
function core.run_priv_callbacks(name, priv, caller, method)
local def = core.registered_privileges[priv]
if not def or not def["on_" .. method] or
not def["on_" .. method](name, caller) then
for _, func in ipairs(core["registered_on_priv_" .. method]) do
if not func(name, caller, priv) then
break
end
end
end
end
--
-- Callback registration
--
local function make_registration()
local t = {}
local registerfunc = function(func)
t[#t + 1] = func
core.callback_origins[func] = {
mod = core.get_current_modname() or "??",
name = debug.getinfo(1, "n").name or "??"
}
--local origin = core.callback_origins[func]
--print(origin.name .. ": " .. origin.mod .. " registering cbk " .. tostring(func))
end
return t, registerfunc
end
local function make_registration_reverse()
local t = {}
local registerfunc = function(func)
table.insert(t, 1, func)
core.callback_origins[func] = {
mod = core.get_current_modname() or "??",
name = debug.getinfo(1, "n").name or "??"
}
--local origin = core.callback_origins[func]
--print(origin.name .. ": " .. origin.mod .. " registering cbk " .. tostring(func))
end
return t, registerfunc
end
local function make_registration_wrap(reg_fn_name, clear_fn_name)
local list = {}
local orig_reg_fn = core[reg_fn_name]
core[reg_fn_name] = function(def)
local retval = orig_reg_fn(def)
if retval ~= nil then
if def.name ~= nil then
list[def.name] = def
else
list[retval] = def
end
end
return retval
end
local orig_clear_fn = core[clear_fn_name]
core[clear_fn_name] = function()
for k in pairs(list) do
list[k] = nil
end
return orig_clear_fn()
end
return list
end
local function make_wrap_deregistration(reg_fn, clear_fn, list)
local unregister = function (key)
if type(key) ~= "string" then
error("key is not a string", 2)
end
if not list[key] then
error("Attempt to unregister non-existent element - '" .. key .. "'", 2)
end
local temporary_list = table.copy(list)
clear_fn()
for k,v in pairs(temporary_list) do
if key ~= k then
reg_fn(v)
end
end
end
return unregister
end
core.registered_on_player_hpchanges = { modifiers = { }, loggers = { } }
function core.registered_on_player_hpchange(player, hp_change, reason)
local last
for i = #core.registered_on_player_hpchanges.modifiers, 1, -1 do
local func = core.registered_on_player_hpchanges.modifiers[i]
hp_change, last = func(player, hp_change, reason)
if type(hp_change) ~= "number" then
local debuginfo = debug.getinfo(func)
error("The register_on_hp_changes function has to return a number at " ..
debuginfo.short_src .. " line " .. debuginfo.linedefined)
end
if last then
break
end
end
for i, func in ipairs(core.registered_on_player_hpchanges.loggers) do
func(player, hp_change, reason)
end
return hp_change
end
function core.register_on_player_hpchange(func, modifier)
if modifier then
core.registered_on_player_hpchanges.modifiers[#core.registered_on_player_hpchanges.modifiers + 1] = func
else
core.registered_on_player_hpchanges.loggers[#core.registered_on_player_hpchanges.loggers + 1] = func
end
core.callback_origins[func] = {
mod = core.get_current_modname() or "??",
name = debug.getinfo(1, "n").name or "??"
}
end
core.registered_biomes = make_registration_wrap("register_biome", "clear_registered_biomes")
core.registered_ores = make_registration_wrap("register_ore", "clear_registered_ores")
core.registered_decorations = make_registration_wrap("register_decoration", "clear_registered_decorations")
core.unregister_biome = make_wrap_deregistration(core.register_biome,
core.clear_registered_biomes, core.registered_biomes)
core.registered_on_chat_messages, core.register_on_chat_message = make_registration()
core.registered_on_chatcommands, core.register_on_chatcommand = make_registration()
core.registered_globalsteps, core.register_globalstep = make_registration()
core.registered_playerevents, core.register_playerevent = make_registration()
core.registered_on_mods_loaded, core.register_on_mods_loaded = make_registration()
core.registered_on_shutdown, core.register_on_shutdown = make_registration()
core.registered_on_punchnodes, core.register_on_punchnode = make_registration()
core.registered_on_placenodes, core.register_on_placenode = make_registration()
core.registered_on_dignodes, core.register_on_dignode = make_registration()
core.registered_on_generateds, core.register_on_generated = make_registration()
core.registered_on_newplayers, core.register_on_newplayer = make_registration()
core.registered_on_dieplayers, core.register_on_dieplayer = make_registration()
core.registered_on_respawnplayers, core.register_on_respawnplayer = make_registration()
core.registered_on_prejoinplayers, core.register_on_prejoinplayer = make_registration()
core.registered_on_joinplayers, core.register_on_joinplayer = make_registration()
core.registered_on_leaveplayers, core.register_on_leaveplayer = make_registration()
core.registered_on_player_receive_fields, core.register_on_player_receive_fields = make_registration_reverse()
core.registered_on_cheats, core.register_on_cheat = make_registration()
core.registered_on_crafts, core.register_on_craft = make_registration()
core.registered_craft_predicts, core.register_craft_predict = make_registration()
core.registered_on_protection_violation, core.register_on_protection_violation = make_registration()
core.registered_on_item_eats, core.register_on_item_eat = make_registration()
core.registered_on_punchplayers, core.register_on_punchplayer = make_registration()
core.registered_on_priv_grant, core.register_on_priv_grant = make_registration()
core.registered_on_priv_revoke, core.register_on_priv_revoke = make_registration()
core.registered_on_authplayers, core.register_on_authplayer = make_registration()
core.registered_can_bypass_userlimit, core.register_can_bypass_userlimit = make_registration()
core.registered_on_modchannel_message, core.register_on_modchannel_message = make_registration()
core.registered_on_player_inventory_actions, core.register_on_player_inventory_action = make_registration()
core.registered_allow_player_inventory_actions, core.register_allow_player_inventory_action = make_registration()
core.registered_on_rightclickplayers, core.register_on_rightclickplayer = make_registration()
core.registered_on_liquid_transformed, core.register_on_liquid_transformed = make_registration()
--
-- Compatibility for on_mapgen_init()
--
core.register_on_mapgen_init = function(func) func(core.get_mapgen_params()) end